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.0

Published

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

Downloads

121

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
←── send sig hex + wallet_id ────────────
clearsign submit <id> <wallet-id> <name> <sig>

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.

5. Anomaly Detection

After every signature submission, the anomaly engine runs automatically:

| Rule | Trigger | Action | |------|---------|--------| | SPEED_SIGNING | All required sigs arrive within 10 s | Auto-void proposal | | HIGH_VALUE | Amount exceeds $50,000 USD | Flag + require independent verification | | OFF_HOURS | Signature outside 08:00–20:00 UTC | Warn signers | | SINGLE_PROCESS | Multiple sigs share identical timestamps | Flag (test/automation guard) |

All thresholds are configurable via AnomalyConfig.

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 (paranoid mode)
  4. Only then calls the actual on-chain transaction / API

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 from npm

npm install clearsign

Or clone and run locally

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

Build

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

CLI

After npm install -g clearsign (or npm 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
CLEARSIGN_SIGNER_NAME="Alice" \
CLEARSIGN_PRIVATE_KEY="<32-byte-hex-private-key>" \
clearsign sign ./proposal.json
# → prints wallet_id and signature hex to send back to coordinator

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>
# → 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 sig = aliceWallet.sign(result.proposal.proposal_hash);
// send sig + 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);
await coordinator.submitSignature(proposal.id, bob.wallet.id,   "Bob",   bobSigHex);

// 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);
await coordinator.submitSignature(proposal.id, bob.wallet.id,   "Bob",   bobSig);

Demo

npm 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

Anomaly Thresholds

import type { AnomalyConfig } from "clearsign";

const config: AnomalyConfig = {
  speed_signing_threshold_secs: 10,      // sigs within 10s → SPEED_SIGNING
  business_hours_utc:           [8, 20], // warn outside 08:00–20:00 UTC
  high_value_threshold_usd:     50_000,  // flag proposals 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.


License

MIT