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

clearsign

v0.1.2

Published

OWS-native multi-sig with human-readable proposals, TTL enforcement, and anomaly detection. Built to prevent blind-signing exploits in DeFi protocols.

Readme

ClearSign

OWS-native multi-sig with human-readable proposals, tamper-proof TTL, and anomaly detection.

ClearSign is a security layer for DeFi treasuries and AI agent spend requests. Every action that requires multi-sig approval is wrapped in a cryptographically-bound, human-readable proposal — signers always see exactly what they are approving before a key is ever touched. Signers can be on completely separate machines; no private key ever reaches the coordinator.


The Problem

Multi-sig setups fail not because the cryptography is weak, but because signers approve things they don't fully understand:

  • Signers are shown raw transaction bytes, not plain-language descriptions.
  • Signatures have no expiry — a stored signature can be replayed indefinitely.
  • There is no shared awareness between signers — a compromised co-signer isn't visible to others.
  • Automated or coerced signing (all sigs in milliseconds) is indistinguishable from legitimate signing.
  • All signers must be on the same machine, so any single compromise exposes every key.

ClearSign addresses all five.


How It Works

1. Human-Readable Proposals

Every action starts as a ProposalV1 — a structured, Zod-validated object containing:

  • Plain-language title and description
  • amount_usd and asset (shown prominently before signing)
  • chain (CAIP-2 format), requester, payload
  • A hard expires_at TTL
  • An m-of-n threshold and the list of authorized signers

Signers see a formatted signing prompt before any key operation. Skipping the review is not possible — the signing call requires the proposal object, which always renders.

2. Proposal Hash = Tamper Proof

Before collecting signatures, ClearSign computes a SHA-256 hash over the immutable content fields of the proposal (action, title, description, amount, chain, payload, signers, expiry). The hash deliberately excludes mutable state (signatures, status, anomaly_flags) so that:

  • The coordinator can re-export an in-progress proposal (with some signatures already collected) to remaining signers without breaking hash verification.
  • All N signers on different machines sign the same hash regardless of how many signatures have been collected so far.
proposal_hash = SHA-256(canonicalize({
  id, version, action, title, description,
  chain, amount_usd, asset, payload, requester,
  threshold, signers, created_at, expires_at
}))

Signers sign this hash — not a raw transaction payload. Altering any content field produces a different hash and invalidates every existing signature.

3. Hard TTL — No Durable Nonces

Every proposal carries an expires_at timestamp baked into its hash. The coordinator auto-expires proposals on read, and the ExecutionGate re-checks status before touching the chain. A signature collected before expiry cannot be used after — there are no durable nonces, no stored-and-replayed attacks.

Default maximum TTL: 30 minutes.

4. Remote Signers — Keys Never Leave the Signer's Machine

The coordinator registers signers by public key only. Private keys are generated and held exclusively on each signer's own machine. The full remote signing flow:

Coordinator machine                        Signer's machine
──────────────────                         ────────────────
                                           clearsign keygen
                                           → public key hex
ClearSignWallet.fromPublicKey(pubKeyHex)
buildProposal({ signers: [...] })
exportProposal(proposal) → proposal.json
─── send proposal.json ──────────────────→
                                           clearsign sign ./proposal.json
                                           → verifies hash
                                           → renders signing prompt
                                           → [y/N] confirm
                                           → signature hex + signed_at
←── send sig hex + wallet_id + signed_at ─
clearsign submit <id> <wallet-id> <name> <sig> <signed-at>

No server required. Private keys never leave the signer's machine. Signatures are verified against the registered public key — the coordinator only needs the public key to check the math.

Signer-side timestamps. signMessage returns { signature, signed_at } where signed_at is captured on the signer's machine, not when the coordinator receives the submission. This is what lets two signers approve a proposal hours apart on separate machines without tripping SPEED_SIGNING when the coordinator batches the submits.

Public-key-only safety. A wallet created via ClearSignWallet.fromPublicKey(...) has no local private key. Calling .signMessage(...) on it throws explicitly — "This wallet was registered by public key only — sign on the signer's own machine" — rather than producing junk output.

5. Anomaly Detection

After every signature submission, the anomaly engine runs automatically:

| Rule | Trigger | Action | |------|---------|--------| | SPEED_SIGNING | All required signer-side timestamps land within 10 s | Auto-void proposal | | HIGH_VALUE | Amount strictly greater than $50,000 USD | Flag + require independent verification | | OFF_HOURS | Signature outside the configured business hours (IANA timezone) | Warn signers | | SINGLE_PROCESS | Multiple sigs share identical timestamps | Flag (test/automation guard) |

All thresholds are configurable via AnomalyConfig. business_hours accepts any IANA timezone ("America/New_York", "Asia/Singapore", "UTC", …) or can be set to null to disable the rule. Findings are emitted as structured { rule, severity, message, details } records — dedup'd by rule — so downstream policy engines can route, suppress, or escalate them programmatically. Legacy string flags from older persisted state are migrated on read.

6. Execution Gate

Even after threshold is met, execution is not automatic. The ExecutionGate:

  1. Confirms proposal is in APPROVED state (not voided, expired, or pending)
  2. Checks no hard anomaly flags (SPEED_SIGNING, SINGLE_PROCESS) are unresolved
  3. Optionally re-verifies every signature from scratch — gate.execute(id, /* paranoid */ true) — without double-counting the first signer
  4. Only then calls the actual on-chain transaction / API

Note: the bundled gate.execute() mocks the final chain call and returns a fake tx_id. Wire in your real Solana / EVM / API submission inside ExecutionGate.mockExecute for production. See Scope & Status.


Architecture

src/
├── index.ts                  # Public API exports
├── demo.ts                   # Interactive 4-scenario demo
├── cli.ts                    # Signer + coordinator CLI (clearsign)
├── gate.ts                   # ExecutionGate — final pre-execution checkpoint
│
├── proposal/
│   ├── schema.ts             # ProposalV1 Zod schema + enums
│   ├── builder.ts            # buildProposal(), hashProposal(), exportProposal(), importProposal()
│   └── renderer.ts           # Terminal rendering (proposal card + signing prompt)
│
├── multisig/
│   ├── coordinator.ts        # Central state machine — accepts sigs, runs anomaly checks
│   ├── verifier.ts           # Ed25519 signature verification, threshold check
│   └── anomaly.ts            # Suspicious pattern detection engine
│
└── wallet/
    └── ows.ts                # OWS wallet abstraction — fromPublicKey(), create(), signMessage()

Security Properties

| Property | Mechanism | |----------|-----------| | No blind signing | Full proposal card rendered before every signMessage call — skipping is impossible | | Tamper-proof payload | proposal_hash commits to content fields — amount, recipient, description, expiry | | No signature replay | Hard TTL baked into hash; expired proposals rejected at coordinator and gate level | | Coercion detection | Speed-signing anomaly auto-voids proposals where sigs arrive within 10 s | | Distributed key custody | fromPublicKey() — each signer holds their own private key on their own machine | | Tamper detection in transit | importProposal() re-computes and verifies hash before any key is touched | | Safe re-export | Hash covers only immutable content — in-progress proposals can be re-shared safely | | Defence-in-depth | Anomaly check at signature time + independent gate check at execution time |


Getting Started

Install with Bun

bun add clearsign

Or clone and run locally

git clone https://github.com/rkmonarch/clearsign.git
cd clearsign
bun install
bun run demo

Build

bun run build      # compile to dist/
bun run typecheck  # type-check only, no emit

Native OWS mode

The CLI defaults to local Ed25519 signing for deterministic demos. In an environment with a clean OWS wallet store, enable the native OWS backend with:

export CLEARSIGN_USE_OWS_NATIVE=1

Deploy the website on Vercel

For the Next.js website in apps/web, import this repository into Vercel as a monorepo project and set:

  • Root Directory: apps/web
  • Framework Preset: Next.js

The app includes apps/web/vercel.json so Vercel installs and builds with Bun.


CLI

After bun add -g clearsign (or bun run build locally), the clearsign CLI is available.

Signer commands

Run these on the signer's own machine.

# Generate a new Ed25519 identity (do this once per signer)
clearsign keygen
# → prints public key (share with coordinator) and private key (keep secret)

# Review a proposal — verify hash and display the full card
clearsign review ./proposal.json

# Sign a proposal — verify hash, render prompt, confirm [y/N], output signature + signed_at
CLEARSIGN_SIGNER_NAME="Alice" \
CLEARSIGN_PRIVATE_KEY="<32-byte-hex-private-key>" \
clearsign sign ./proposal.json
# → prints wallet_id, signature hex, signed_at, and a ready-to-run submit command

Coordinator commands

Run these on the coordinator machine.

# List all pending proposals
clearsign list

# Show full status of a proposal
clearsign status <proposal-id>

# Record a remote signer's signature
clearsign submit <proposal-id> <wallet-id> <signer-name> <sig-hex> <signed-at>
# → reports: signature_accepted | threshold_met | proposal_voided

Integration

Remote signers (recommended)

Each signer generates their keypair independently. The coordinator only holds public keys.

import {
  ClearSignWallet,
  buildProposal,
  exportProposal,
  importProposal,
  MultiSigCoordinator,
  ExecutionGate,
  ProposalAction,
  DEFAULT_ANOMALY_CONFIG,
} from "clearsign";

// 1. Signers run `clearsign keygen` on their own machines and share public keys
const alice = ClearSignWallet.fromPublicKey("Alice", alicePubKeyHex, "solana:mainnet");
const bob   = ClearSignWallet.fromPublicKey("Bob",   bobPubKeyHex,   "solana:mainnet");

// 2. Build a proposal
const proposal = buildProposal({
  action:      ProposalAction.TRANSFER,
  title:       "Pay auditors — Q2 2026",
  description: "Transfer 10,000 USDC to Trail of Bits for Q2 audit.",
  chain:       "solana:mainnet",
  amount_usd:  10_000,
  asset:       "USDC",
  payload:     { instruction: "transfer", to: "toB...", amount: "10000000000" },
  requester:   "ops-agent",
  threshold:   { required: 2, total: 2 },
  signers: [
    { wallet_id: alice.wallet.id, public_key: alice.publicKeyHex, name: "Alice" },
    { wallet_id: bob.wallet.id,   public_key: bob.publicKeyHex,   name: "Bob" },
  ],
  ttl_minutes: 30,
});

// 3. Export and send to signers (email, Slack, API — any channel)
const proposalJson = exportProposal(proposal);

// ── On each signer's machine: ──────────────────────────────────────────────
// const result = importProposal(proposalJson);  // verifies hash — rejects tampering
// if (!result.ok) throw new Error(result.reason);
// const signed = await aliceWallet.signMessage(result.proposal.proposal_hash);
// send signed.signature + signed.signed_at + wallet_id back to coordinator
// ──────────────────────────────────────────────────────────────────────────

// 4. Coordinator collects signatures
const coordinator = new MultiSigCoordinator(DEFAULT_ANOMALY_CONFIG);
await coordinator.init();
await coordinator.addProposal(proposal);
await coordinator.submitSignature(proposal.id, alice.wallet.id, "Alice", aliceSigHex, aliceSignedAt);
await coordinator.submitSignature(proposal.id, bob.wallet.id,   "Bob",   bobSigHex,   bobSignedAt);

// 5. Execute through the gate
const gate   = new ExecutionGate(coordinator);
const result = await gate.execute(proposal.id);

Local signers (single machine / testing)

const alice = await ClearSignWallet.create("Alice", "solana:mainnet");
const bob   = await ClearSignWallet.create("Bob",   "solana:mainnet");

const proposal = buildProposal({ ..., signers: [...] });

const aliceSig = await alice.signMessage(proposal.proposal_hash!);
const bobSig   = await bob.signMessage(proposal.proposal_hash!);

await coordinator.submitSignature(proposal.id, alice.wallet.id, "Alice", aliceSig.signature, aliceSig.signed_at);
await coordinator.submitSignature(proposal.id, bob.wallet.id,   "Bob",   bobSig.signature,   bobSig.signed_at);

Demo

bun run demo

Four scenarios, end-to-end:

| Scenario | What happens | |----------|-------------| | A — Legitimate transfer | Coordinator registers 2 remote signers by public key. Builds and exports proposal. Each signer imports, verifies hash, signs. Coordinator re-exports in-progress proposal (with one sig) to second signer — hash still verifies. Threshold met, gate executes. | | B — Blind-signing attack | Attacker submits malicious $280M drain. Both sigs arrive within milliseconds → SPEED_SIGNING anomaly → proposal auto-voided. Gate blocks execution. | | C — TTL expiry | Alice tries to sign an already-expired proposal. Rejected at coordinator level. No durable nonces possible. | | D — 3 remote signers, 3 machines | 2-of-3 contract upgrade. Each signer independently imports, verifies, and signs. Signatures arrive out of order — Carol before Bob. Threshold met, late sig gracefully rejected, gate executes upgrade. |


Configuration

Coordinator Storage

By default the coordinator writes proposal state to ~/.clearsign/proposals.json. Treat this as ephemeral coordination state — it may contain signed proposal metadata and is not encrypted at rest. For tests, demos, stateless servers, or custom encrypted storage, pass options explicitly:

const coordinator = new MultiSigCoordinator({
  anomalyConfig: DEFAULT_ANOMALY_CONFIG,
  inMemory:  true,                              // skip disk entirely (tests / stateless servers)
  storePath: "/secure/path/proposals.json",     // custom store (mount an encrypted volume here)
  logLoads:  false,                             // suppress "[ClearSign] Loaded N proposal(s)" stderr line
});

If you run the coordinator long-lived in production, plan for one of: mount an encrypted volume at storePath, run with inMemory: true and back the state with your own durable store, or post the proposal hash to an on-chain registry for the system-of-record. See Scope & Status.

Anomaly Thresholds

import type { AnomalyConfig } from "clearsign";

const config: AnomalyConfig = {
  speed_signing_threshold_secs: 10,                  // sigs within 10s → SPEED_SIGNING
  business_hours:               [9, 17],             // warn outside 09:00–17:00 local
  business_hours_tz:            "America/New_York",  // IANA tz; use "UTC" or set business_hours: null to disable
  high_value_threshold_usd:     50_000,              // flag proposals strictly above $50k
  auto_void_on_speed:           true,                // auto-void vs just flag
};

TTL

Maximum TTL is enforced at buildProposal time — any ttl_minutes above 30 is clamped to 30. To change the cap, update MAX_TTL_MINUTES in src/proposal/builder.ts.


Scope & Status

ClearSign ships the safety primitives for human-readable, hash-locked, time-bound multi-sig — not a finished treasury product. What's in the box vs what an integrator still owns:

| In the box | Out of scope (you wire it) | |------------|----------------------------| | ProposalV1 schema + canonical hash | A GUI for non-CLI signers (build with the SDK + your design system) | | ClearSignWallet.fromPublicKey() — coordinator never holds private keys | Hardware wallet / wallet-adapter integration on the signer side | | MultiSigCoordinator — in-process state machine with optional JSON persistence | A hosted multi-tenant coordinator service (auth, REST/gRPC, audit log) | | Anomaly detection engine — configurable, dedup'd, structured flags | On-chain anchor / attestation program for system-of-record | | ExecutionGate — anomaly + status + paranoid re-verification | Real chain submission inside gate.execute (mocked today) | | CLI for signer + coordinator commands | Push notifications / proposal transport (today: pass the JSON over your channel of choice) | | Tamper-evident exportProposal / importProposal | Admin actions for signer rotation (today: rebuild the proposal model) |

If you need the missing pieces today, you build them on top — the primitives are stable and the surface is small.

Roadmap

  • On-chain anchor program — post proposal hash + signer set + threshold to a Solana program at creation; signatures recorded as on-chain attestations so auditors and the gate can verify history independently of any one coordinator.
  • Reference hosted coordinator — REST + auth + audit log + push notifications. Single-tenant deployable, multi-tenant ready.
  • Wallet-Standard integration — render the ClearSign signing card inside any wallet that opts in (Phantom, Backpack, Solflare, Ledger), instead of inside the CLI.
  • Production execution adapter — drop-in ExecutionGate for Solana SystemProgram.transfer, SPL token transfer, and arbitrary TransactionMessage payloads.

Changelog

0.1.1

  • Signer-side timestamps end-to-end. signMessage() now returns { signature, signed_at } and submitSignature(..., signedAt) records the signer's timestamp instead of the coordinator's receive time. Two signers approving hours apart on separate machines no longer trip SPEED_SIGNING.
  • Structured anomaly flags. anomaly_flags is now { rule, severity, message, details }[] instead of opaque strings; dedup'd by rule (with per-signer keying for OFF_HOURS). Legacy string flags from older persisted state are migrated on read.
  • OFF_HOURS per IANA timezone. business_hours accepts any timezone ("America/New_York", "Asia/Singapore", …), and business_hours: null disables the rule entirely.
  • HIGH_VALUE is strictly greater. A proposal exactly at the threshold no longer trips the rule.
  • ExecutionGate paranoid mode fixed. gate.execute(id, true) re-verifies every signature without double-counting the first signer.
  • ClearSignWallet.fromPublicKey() throws on signMessage with an explicit message instead of failing downstream.
  • MultiSigCoordinator options. { inMemory, storePath, logLoads } for test isolation, stateless servers, and quieter CLI output. Backwards-compatible with the prior AnomalyConfig-only constructor signature.
  • Locale-pinned formatting. Amounts render in en-US; expiry timestamps render as UTC ISO. No locale leaks into proposal output.

License

MIT