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

@caterpillar-labs/zeos-link

v0.4.0

Published

Browser SDK for connecting web apps to the local ZEOS Link WebSocket service.

Readme

@caterpillar-labs/zeos-link

Browser SDK for connecting web apps to the local CLOAK / ZEOS Link wallet service.

@caterpillar-labs/zeos-link is a tiny TypeScript SDK that talks to the CLOAK desktop wallet over a local secure WebSocket connection. It lets a web app request login approval, query private wallet balances, and submit shielded ZEOS actions for wallet-side proving, signing, and publishing.

The default connection target is:

wss://127.0.0.1:9367

This package is intentionally small. It is not a general EOSIO wallet SDK, not a WharfKit replacement, and not a React state manager. It is only the browser-side client for the CLOAK wallet's local ZEOS Link protocol.


TL;DR

Use this package when a web app wants to support the CLOAK wallet.

import ZSession, { ALL_WALLET_CONTRACTS, type ChainParams, type ZAction } from "@caterpillar-labs/zeos-link";

const session = new ZSession();

const chain: ChainParams = {
  chain_id: "aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906",
  protocol_contract: "zeos4privacy",
  vault_contract: "thezeosvault",
  alias_authority: "thezeosalias@public",
};

const login = await session.login(chain);

if (!login) {
  // User declined, wallet network mismatch, or wallet rejected login.
  return;
}

const balances = await session.allBalances({
  ft: true,
  nftContract: ALL_WALLET_CONTRACTS,
  atContract: ALL_WALLET_CONTRACTS,
});

const zactions: ZAction[] = [
  {
    name: "spend",
    data: {
      contract: "eosio.token",
      change_to: "$SELF",
      publish_change_note: true,
      to: [
        {
          to: "alice",
          quantity: "1.0000 EOS",
          memo: "hello",
          publish_note: true,
        },
      ],
    },
  },
];

const result = await session.transact(zactions, true, true, {
  timeoutMs: 120_000,
});

if (result.status === "error") {
  console.error(result.error);
  return;
}

console.log(result);

Important:

  • The CLOAK wallet must be running locally.
  • The wallet exposes a secure WebSocket server on wss://127.0.0.1:9367.
  • Login opens a native wallet approval dialog.
  • Balance requests may open a native wallet approval dialog.
  • Transactions open a native wallet signature dialog.
  • login() returns null for expected wallet rejection/decline.
  • Balance protocol errors throw.
  • transact() returns successful transaction responses and structured transaction error responses.
  • Network/socket/timeout failures throw.
  • This SDK only supports ZEOS/CLOAK shielded zactions, not Anchor/WharfKit { actions: [...] } transactions.

Installation

npm install @caterpillar-labs/zeos-link

The old unscoped zeos-link package has moved to this scoped package.


Usage with npm / bundlers

import ZSession from "@caterpillar-labs/zeos-link";

const session = new ZSession();

Named import also works:

import { ZSession } from "@caterpillar-labs/zeos-link";

Import types:

import type {
  ChainParams,
  ZAction,
  MintAction,
  SpendAction,
  AuthenticateAction,
  PublishNotesAction,
  WithdrawAction,
  BalancesResult,
  TransactResult,
} from "@caterpillar-labs/zeos-link";

Usage as a browser ES module

You can copy the built browser file into your public assets and import it directly:

<script type="module">
  import ZSession from "/zeos-link.js";

  const session = new ZSession();
</script>

When installed from npm, the browser ESM build is available at:

node_modules/@caterpillar-labs/zeos-link/dist/zeos-link.js

Usage as a global browser script

The package also builds a global script for projects that do not use ESM.

<script src="/zeos-link.global.js"></script>
<script>
  const session = new ZEOSLink.ZSession();
</script>

Prefer the ESM build for modern apps.


What this SDK does

@caterpillar-labs/zeos-link handles:

  • opening/reusing a WebSocket connection to the local CLOAK wallet,
  • sending request frames with unique request ids,
  • correlating wallet replies back to the pending request,
  • timing out stale requests,
  • converting expected login rejection into null,
  • throwing typed protocol/connection/timeout errors where appropriate,
  • routing id-less server errors such as rate-limit errors to the pending request when safe,
  • handling known wallet-server behavior such as uncorrelated transaction error frames.

What this SDK does not do

This SDK does not:

  • manage React state,
  • store wallet sessions in localStorage,
  • choose the app's active network,
  • format balances for UI,
  • resolve token icons,
  • support Anchor/WharfKit/native EOSIO transaction shapes,
  • validate your dapp's business rules,
  • replace server-side authorization or transaction validation.

Keep those responsibilities in your app.


CLOAK wallet / ZEOS Link architecture

The CLOAK desktop wallet runs a local secure WebSocket server.

web app
  |
  |  wss://127.0.0.1:9367
  v
CLOAK desktop wallet
  |
  |  native approval/signature dialogs
  v
ZEOS wallet core / chain RPC

The wallet listens on localhost only. This is intentional: the desktop wallet acts as a local signer, not as a remote public API.

The browser app sends JSON request frames. The wallet validates them, optionally shows a native Qt approval dialog, and replies with JSON response frames.


Protocol frame shape

Every SDK request uses this shape:

{
  "id": 1,
  "request": "login",
  "params": {}
}

Normal responses echo the request id:

{
  "id": 1,
  "status": "success",
  "result": {}
}

Error responses usually echo the request id:

{
  "id": 1,
  "status": "error",
  "error": "not logged in"
}

Some low-level server errors may not include an id, for example rate limiting or message-size rejection. The SDK handles id-less status: "error" frames by routing them to the only pending request when there is exactly one pending request.


Supported protocol requests

The CLOAK wallet currently supports these request names:

login
all_balances
balances
transact

Unknown requests receive:

{
  "status": "error",
  "error": "unknown request"
}

Public API

export class ZSession {
  constructor(url?: string, options?: SessionOptions);

  login(chain: ChainParams, onClose?: () => void): Promise<LoginResult | null>;
  logout(): void;

  isConnected(): boolean;
  handle(): string | null;

  allBalances(
    ft?: boolean,
    nft?: boolean,
    at?: boolean,
    opts?: RequestOptions
  ): Promise<BalancesResult>;

  balances(
    ftSymbols?: string[],
    nftContract?: string,
    atContract?: string,
    opts?: RequestOptions
  ): Promise<BalancesResult>;

  transact(
    zactions: ZAction[],
    addFee?: boolean,
    publishFeeNote?: boolean,
    opts?: RequestOptions
  ): Promise<TransactResult>;
}

export default ZSession;

Login

API

const result = await session.login(chain, onClose);

Type

login(
  chain: ChainParams,
  onClose?: () => void
): Promise<LoginResult | null>

Chain params

interface ChainParams {
  chain_id: string;
  protocol_contract: string;
  vault_contract: string;
  alias_authority: string;
}

Example:

const login = await session.login({
  chain_id: "aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906",
  protocol_contract: "zeos4privacy",
  vault_contract: "thezeosvault",
  alias_authority: "thezeosalias@public",
});

Login behavior

The wallet validates that the provided login fields match the wallet's currently active network configuration.

The wallet checks:

chain_id
protocol_contract
vault_contract
alias_authority

alias_authority is expected in this form:

account@permission

Example:

thezeosalias@public

If the params do not match the wallet's active network, the wallet rejects the login.

If the params match, the wallet opens a native login approval dialog. The user must accept the request in the CLOAK wallet.

Login result

On approval:

const login = await session.login(chain);

if (login) {
  console.log("Connected", login.result);
}

On expected rejection:

const login = await session.login(chain);

if (!login) {
  // User declined, wallet rejected login, or active wallet network did not match.
}

login() returns null for expected wallet-level rejection. It throws only for transport/runtime failures such as connection errors, malformed replies, or timeouts.

Important: do not treat the login result as a public account

The CLOAK wallet may return an opaque/private handle such as "anonymous". This is not a normal public EOSIO account identity. Do not use it as proof of account ownership.


Logout

session.logout();

This closes the WebSocket connection, clears the locally stored chain params and handle, and rejects pending requests.

It does not alter wallet state inside the desktop wallet.


Connection status

session.isConnected();

Returns whether the underlying WebSocket is currently open.

session.handle();

Returns the current wallet handle from the login response, or null.

Again: the handle is not a public EOSIO account identity.


Query all balances

API

import { ALL_WALLET_CONTRACTS } from "@caterpillar-labs/zeos-link";

const balances = await session.allBalances({
  ft: true,
  nftContract: ALL_WALLET_CONTRACTS,
  atContract: "thezeosalias",
});

Type

allBalances(
  params?: AllBalancesParams,
  opts?: RequestOptions
): Promise<BalancesResult>

interface AllBalancesParams {
  ft?: boolean;
  nftContract?: string;
  atContract?: string;
}

Request params

The SDK sends the wire shape supported by the CLOAK desktop wallet:

{
  "request": "all_balances",
  "params": {
    "ft": true,
    "nft_contract": "",
    "at_contract": "thezeosalias"
  }
}

Meaning:

ft            = include fungible token balances (`fts`)
nft_contract  = NFT contract filter; `""` means all contracts (wallet contract id 0)
at_contract   = auth-token contract filter; `""` means all contracts (wallet contract id 0)

Omit nftContract / atContract to skip those sections entirely.

Result shape

Typical result:

interface BalancesResult {
  fts?: string[];
  nfts?: unknown[] | string[];
  ats?: {
    spent: string[];
    unspent: string[];
  } | string[];
}

Example:

const balances = await session.allBalances({ ft: true });

console.log(balances.fts);

User approval

The wallet may show a native balance-request approval dialog. Do not assume this is a silent background query.


Query filtered balances

API

const balances = await session.balances(
  ["4,EOS", "8,CLOAK"],
  "atomicassets",
  "theauthcontr",
);

Type

balances(
  ftSymbols?: string[],
  nftContract?: string,
  atContract?: string,
  opts?: RequestOptions
): Promise<BalancesResult>

Request params

balances() calls all_balances on the wallet and filters fts client-side when ftSymbols is provided:

{
  "request": "all_balances",
  "params": {
    "ft": true,
    "nft_contract": "atomicassets",
    "at_contract": "theauthcontr"
  }
}

The desktop wallet does not expose a separate balances request.

Fungible token symbol format

Use EOSIO-style symbol strings:

precision,SYMBOL

Examples:

4,EOS
8,CLOAK
3,UN

When a requested fungible token balance is not found, the wallet may return a zero balance for that symbol.


Transact

API

const result = await session.transact(zactions);

Type

transact(
  zactions: ZAction[],
  addFee?: boolean,
  publishFeeNote?: boolean,
  opts?: RequestOptions
): Promise<TransactResult>

Defaults

addFee         true
publishFeeNote true
timeoutMs      60000

For proof-heavy flows, use a longer timeout:

const result = await session.transact(zactions, true, true, {
  timeoutMs: 120_000,
});

The SDK sends a transact request with the active login chain params:

{
  "request": "transact",
  "params": {
    "chain_id": "...",
    "protocol_contract": "...",
    "vault_contract": "...",
    "alias_authority": "...",
    "add_fee": true,
    "publish_fee_note": true,
    "zactions": []
  }
}

Exact wallet-resolved transaction fee

Compatible CLOAK wallets include the exact fee actually resolved and paid by the private wallet in successful transaction responses:

const result = await session.transact(zactions);

if (result.status === "success" && result.payload?.tx_fee) {
  console.log(result.payload.tx_fee);
  // Example: "0.0123 CLOAK@thezeostoken"
}

payload.tx_fee is a canonical ExtendedAsset string (quantity@contract). It is calculated by the wallet after private-note selection; the SDK does not estimate or recompute it. The field is optional so older wallets and transactions without reported fee metadata remain compatible. Treat it as a confirmed wallet balance effect only on a successful transaction response.

Important: this is not Anchor / WharfKit

This is valid for CLOAK / ZEOS Link:

await session.transact([
  {
    name: "spend",
    data: {
      contract: "eosio.token",
      change_to: "$SELF",
      publish_change_note: true,
      to: [
        {
          to: "alice",
          quantity: "1.0000 EOS",
          memo: "",
          publish_note: true,
        },
      ],
    },
  },
]);

This is not a ZEOS Link transaction:

await session.transact({
  actions: [
    {
      account: "eosio.token",
      name: "transfer",
      data: {},
    },
  ],
});

{ actions: [...] } belongs to Anchor, WharfKit, or other native EOSIO wallet/session APIs. Do not mix those shapes into this SDK.


ZActions guide

zactions are the high-level private actions sent to the CLOAK wallet through:

await session.transact(zactions);

The public TypeScript type is:

type ZAction =
  | MintAction
  | SpendAction
  | AuthenticateAction
  | PublishNotesAction
  | WithdrawAction;

The wallet receives these high-level JSON descriptions, resolves them against the wallet state, creates the necessary zero-knowledge proofs, signs/publishes the resulting protocol transaction, and returns a transaction result.

The supported action names are:

mint
spend
authenticate
publishnotes
withdraw

Common string formats

EOSIO account/name:
  "eosio.token"
  "atomicassets"
  "mycontract"

Authorization:
  "actor@permission"
  "mycontract@active"

FT quantity:
  "10.0000 EOS"

NFT quantity:
  "123456789"

Symbol filter:
  "4,EOS"

Shielded address:
  "za1..."

Self placeholder:
  "$SELF"

Auth token placeholder:
  "$AUTH0" ... "$AUTH9"

Existing auth token commitment:
  64-char hex string

NFTs use symbol raw value 0, conceptually equivalent to symbol string "0,". Since normal asset strings do not represent that nicely, ZEOS/CLOAK represents NFT quantities as pure integer asset-id strings, for example:

"123456789"

Placeholders

$SELF means the current wallet's default shielded address.

$AUTH0 ... $AUTH9 refer to auth tokens minted earlier in the same transaction. This lets a transaction mint an auth token and immediately use it in a later authenticate action without the frontend knowing the final commitment beforehand.

Memos may also contain:

$SELF
$AUTH0 ... $AUTH9

The wallet resolves those placeholders during transaction construction.


mint

Creates a new shielded note.

const zactions: ZAction[] = [
  {
    name: "mint",
    data: {
      to: "$SELF",
      contract: "eosio.token",
      quantity: "10.0000 EOS",
      memo: "",
      from: "alice",
      publish_note: true,
    },
  },
];

Type shape:

interface MintAction {
  name: "mint";
  data: {
    to: string;
    contract: string;
    quantity: string;
    memo: string;
    from: string;
    publish_note: boolean;
  };
}

Field notes:

to:
  "$SELF" or shielded address

contract:
  token/NFT/auth-token contract account

quantity:
  FT: "10.0000 EOS"
  NFT: "123456789"
  auth-token mint: "0"

from:
  EOSIO account that funded the protocol asset buffer

publish_note:
  whether the encrypted note should be published for recipient discovery

Auth token minting is a special case of mint where quantity is "0".

For auth token mints, from must equal contract. The wallet/protocol rejects auth token mints where the auth token source account and contract do not match.


spend

Spends existing shielded notes to shielded recipients, unshielded EOSIO accounts, or both.

const zactions: ZAction[] = [
  {
    name: "spend",
    data: {
      contract: "eosio.token",
      change_to: "$SELF",
      publish_change_note: true,
      to: [
        {
          to: "bob",
          quantity: "5.0000 EOS",
          memo: "public output",
          publish_note: true,
        },
        {
          to: "za1...",
          quantity: "2.0000 EOS",
          memo: "shielded output",
          publish_note: true,
        },
      ],
    },
  },
];

Type shape:

interface SpendAction {
  name: "spend";
  data: {
    contract: string;
    change_to: string;
    publish_change_note: boolean;
    to: Array<{
      to: string;
      quantity: string;
      memo: string;
      publish_note: boolean;
    }>;
  };
}

Recipient rules:

"$SELF":
  current wallet default shielded address

"za1...":
  shielded address

<=12-char EOSIO name:
  unshielded EOSIO account recipient

64-char hex string:
  auth/vault recipient hash

FT spend quantity example:

"10.0000 EOS"

NFT spend quantity example:

"123456789"

authenticate

Privately authorizes EOSIO actions using an auth token.

This is the key dapp-integration action. It lets a dapp define private actions that are authorized by a ZEOS auth token instead of a normal public account signature.

const zactions: ZAction[] = [
  {
    name: "authenticate",
    data: {
      auth_token: "$AUTH0",
      burn: true,
      actions: [
        {
          account: "mycontract",
          name: "claimauctiop",
          authorization: ["mycontract@active"],
          data: {
            round: 7,
          },
        },
      ],
    },
  },
];

Type shape:

interface AuthenticateAction {
  name: "authenticate";
  data: {
    auth_token: string;
    burn: boolean;
    actions: Array<{
      account: string;
      name: string;
      authorization: string[];
      data: Record<string, unknown>;
    }>;
  };
}

Important: actions[].data is normal unpacked EOSIO JSON action data, exactly like native EOSIO wallets accept.

Do not pass packed hex here.

Good:

data: { round: 7 }

Bad:

data: "deadbeef"

The CLOAK wallet packs this JSON action data internally using the chain ABI before the Rust transaction resolver receives it.

Auth token references

auth_token may be:

"$AUTH0" ... "$AUTH9"

for auth tokens minted earlier in the same transaction, or:

64-char hex commitment

for an existing unspent auth token.

Private dapp action pattern

A dapp can expose a public action and a private/authenticated variant.

Example:

ACTION claimauction(const eosio::name& owner, const uint32_t& round);
ACTION claimauctiop(const uint32_t& round);
ZAUTHENTICATE(ZACTION(claimauctiop))

The private frontend sends:

const zactions: ZAction[] = [
  {
    name: "authenticate",
    data: {
      auth_token: String(authTokenCommitment),
      burn: true,
      actions: [
        {
          account: "mycontract",
          name: "claimauctiop",
          authorization: ["mycontract@active"],
          data: { round },
        },
      ],
    },
  },
];

The protocol verifies the auth proof, then notifies the authenticated contract. The dapp contract reads the authenticated action buffer and executes allowed private actions.

Troubleshooting authenticate

authenticate can fail if:

- auth_token is invalid, spent, or unavailable
- burn is wrong for the intended flow
- nested account/name is wrong
- nested action is not in the dapp contract ABI
- nested data does not match the ABI
- chain RPC cannot fetch ABI / pack action data
- the dapp contract does not allow the private action in its authenticate handler

publishnotes

Publishes encrypted note ciphertexts.

const zactions: ZAction[] = [
  {
    name: "publishnotes",
    data: {
      notes: ["...base64-note-ciphertext..."],
    },
  },
];

Type shape:

interface PublishNotesAction {
  name: "publishnotes";
  data: {
    notes: string[];
  };
}

Most frontend apps should not invent these strings manually. They usually come from wallet/protocol flows.


withdraw

Drains assets from the shielded protocol contract's asset buffer to an unshielded EOSIO account.

This is not merely "withdraw from privacy wallet." It is useful in complex private DeFi flows where the shielded protocol contract temporarily acts as the asset-holding account and receives assets that should be sent out again instead of immediately being minted into shielded UTXOs.

const zactions: ZAction[] = [
  {
    name: "withdraw",
    data: {
      contract: "eosio.token",
      quantity: "10.0000 EOS",
      memo: "settlement",
      to: "alice",
    },
  },
];

Type shape:

interface WithdrawAction {
  name: "withdraw";
  data: {
    contract: string;
    quantity: string;
    memo: string;
    to: string;
  };
}

FT quantity example:

"10.0000 EOS"

NFT quantity example:

"123456789"

The protocol checks the asset buffer, matches the requested contract/symbol/value, and sends the asset out from the protocol contract to to.


Request options

Most request methods accept:

interface RequestOptions {
  timeoutMs?: number;
}

Examples:

await session.allBalances(
  { ft: true, nftContract: ALL_WALLET_CONTRACTS, atContract: ALL_WALLET_CONTRACTS },
  { timeoutMs: 30_000 },
);

await session.transact(zactions, true, true, {
  timeoutMs: 120_000,
});

Recommended defaults:

login        30s fixed internally
balances     15s
transact     60s or longer for proof-generation-heavy flows

Transaction signing can be slow because the wallet may need to resolve, prove, sign, and publish a shielded transaction.


Error contract

This is the most important API contract.

Login

login approved
  -> resolves LoginResult

user declined login
  -> resolves null

wallet network mismatch
  -> resolves null

wallet rejects login params
  -> resolves null

socket/network/timeout/runtime failure
  -> throws

Use:

try {
  const login = await session.login(chain);

  if (!login) {
    // Expected wallet-level rejection.
    return;
  }

  // Connected.
} catch (err) {
  // Transport/runtime failure.
}

Balances

balance request approved
  -> resolves BalancesResult

wallet protocol error
  -> throws ProtocolError

rate limited / message too large
  -> throws protocol-style error

socket/network/timeout/runtime failure
  -> throws

Use:

try {
  const balances = await session.allBalances({
  ft: true,
  nftContract: ALL_WALLET_CONTRACTS,
  atContract: ALL_WALLET_CONTRACTS,
});
} catch (err) {
  // Show a real error. Do not treat as "no balances".
}

Transact

transaction approved and processed
  -> resolves TransactResult with status: "success"

transaction rejected/failed at wallet/protocol level
  -> resolves TransactResult with status: "error"

uncorrelated transaction error frame from wallet
  -> resolves structured transaction error result

socket/network/timeout/runtime failure
  -> throws

Use:

try {
  const result = await session.transact(zactions);

  if (result.status === "error") {
    // Wallet/protocol-level transaction failure.
    console.error(result.error);
    return;
  }

  // Success.
} catch (err) {
  // Transport/runtime failure.
}

Why transaction errors resolve instead of throw:

A transaction can fail after the wallet has accepted the request and attempted to resolve/sign/publish. That is a wallet/protocol result, not necessarily a broken SDK transport. Apps should inspect result.status.


Error handling pattern

Recommended app-side helper:

import {
  ProtocolError,
  TimeoutError,
  ConnectionError,
  SendError,
} from "@caterpillar-labs/zeos-link";

function describeCloakError(err: unknown): string {
  if (err instanceof TimeoutError) {
    return "The CLOAK wallet did not respond in time.";
  }

  if (err instanceof ConnectionError) {
    return "Could not connect to the local CLOAK wallet.";
  }

  if (err instanceof ProtocolError) {
    return err.message || "The CLOAK wallet rejected the request.";
  }

  if (err instanceof SendError) {
    return err.message || "Could not send the request to the CLOAK wallet.";
  }

  if (err instanceof Error) {
    return err.message;
  }

  return "Unknown CLOAK wallet error.";
}

Then:

try {
  const balances = await session.allBalances({
  ft: true,
  nftContract: ALL_WALLET_CONTRACTS,
  atContract: ALL_WALLET_CONTRACTS,
});
} catch (err) {
  notifyUser(describeCloakError(err));
}

Detecting whether CLOAK wallet is available

The simplest check is attempting login.

const session = new ZSession();

try {
  const login = await session.login(chain);

  if (!login) {
    console.log("Wallet rejected login or user declined.");
  }
} catch (err) {
  console.log("CLOAK wallet is unavailable or unreachable.");
}

Common reasons connection fails:

- CLOAK desktop wallet is not running
- no wallet is open inside CLOAK
- local WSS server is not listening
- browser rejected the local TLS certificate
- browser/app origin is blocked by future wallet origin policy
- local firewall/proxy/security software interferes with localhost WSS

React integration pattern

Keep the SDK instance in app wallet state, not inside random components.

Example sketch:

import ZSession from "@caterpillar-labs/zeos-link";

const session = new ZSession();

const login = await session.login(chain, () => {
  // Wallet socket closed.
  // Clear app wallet state here.
});

if (!login) {
  // User declined or wallet rejected.
  return;
}

// Store session as the active CLOAK wallet session.
walletSessionRef.current = session;
walletTypeRef.current = "CLOAK";

After transaction:

const result = await session.transact(zactions, true, true, {
  timeoutMs: 120_000,
});

if (result.status === "error") {
  // Show transaction error.
  return;
}

// Refresh balances / local app state.

Do not expose ZSession internals to UI components. Wrap it in your app's wallet adapter.


Suggested app adapter boundary

Good:

app wallet adapter
  - knows about React state
  - knows selected network
  - knows token icons
  - knows notifications
  - owns walletSessionRef
  - imports ZSession from @caterpillar-labs/zeos-link

Bad:

ZEOS Link SDK
  - imports React app types
  - knows about token icons
  - knows about app notifications
  - supports Anchor/WharfKit transaction shapes

Keep the SDK boring and protocol-focused.


Security notes

Localhost only

The CLOAK wallet is expected to listen on localhost:

wss://127.0.0.1:9367

Do not expose the wallet WSS server on a public network interface.

Validate chain params in the app

The SDK validates basic string shape. Your app is still responsible for choosing the correct network config.

Wrong chain params should fail login, but do not rely on wallet rejection as your only safety layer.

Treat wallet dialogs as the security boundary

Login, balance reads, and transactions can show native wallet dialogs. Design UX around that.

Do not spam wallet prompts.

Do not assume login means public account identity

CLOAK is a privacy wallet. The login result is an opaque wallet handle, not a public account proof.

Pin CDN versions

If loading from a CDN, pin exact versions.

Good:

<script type="module">
  import ZSession from "https://unpkg.com/@caterpillar-labs/[email protected]/dist/zeos-link.js";
</script>

Bad:

<script type="module">
  import ZSession from "https://unpkg.com/@caterpillar-labs/zeos-link@latest/dist/zeos-link.js";
</script>

Never auto-submit sensitive transactions

Always let the wallet approval/signature dialog be visible to the user. The dapp should make it clear what the user is about to do before calling transact().


Raw protocol examples

You normally do not need this when using the SDK, but it is useful for debugging and for AI agents reading the repo.

Login request

{
  "id": 1,
  "request": "login",
  "params": {
    "chain_id": "aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906",
    "protocol_contract": "zeos4privacy",
    "vault_contract": "thezeosvault",
    "alias_authority": "thezeosalias@public"
  }
}

Login success

{
  "id": 1,
  "status": "success",
  "result": "anonymous"
}

Login rejection

{
  "id": 1,
  "status": "error",
  "error": "declined"
}

or:

{
  "id": 1,
  "status": "error",
  "error": "login declined"
}

All balances request

{
  "id": 2,
  "request": "all_balances",
  "params": {
    "ft": true,
    "nft_contract": "",
    "at_contract": "thezeosalias"
  }
}

Filtered balances (balances() helper)

balances() uses all_balances and filters fts in the SDK:

{
  "id": 3,
  "request": "all_balances",
  "params": {
    "ft": true,
    "nft_contract": "atomicassets",
    "at_contract": "theauthcontr"
  }
}

Transact request

{
  "id": 4,
  "request": "transact",
  "params": {
    "chain_id": "...",
    "protocol_contract": "...",
    "vault_contract": "...",
    "alias_authority": "...",
    "add_fee": true,
    "publish_fee_note": true,
    "zactions": [
      {
        "name": "spend",
        "data": {
          "contract": "eosio.token",
          "change_to": "$SELF",
          "publish_change_note": true,
          "to": [
            {
              "to": "alice",
              "quantity": "1.0000 EOS",
              "memo": "",
              "publish_note": true
            }
          ]
        }
      }
    ]
  }
}

Error response

{
  "id": 4,
  "status": "error",
  "error": "not logged in"
}

Id-less low-level error

{
  "status": "error",
  "error": "rate limited"
}

The SDK handles this if exactly one request is pending.


Development

Install dependencies:

npm install

Typecheck:

npm run typecheck

Run tests:

npm test

Build:

npm run build

The build outputs:

dist/index.mjs
dist/index.cjs
dist/index.d.ts
dist/index.d.cts
dist/zeos-link.js
dist/zeos-link.min.js
dist/zeos-link.global.js

Smoke test package exports

From outside the repo:

mkdir /tmp/zeos-link-smoke
cd /tmp/zeos-link-smoke
npm init -y
npm install @caterpillar-labs/zeos-link

Test ESM:

node -e "import('@caterpillar-labs/zeos-link').then(m => console.log(typeof m.default, typeof m.ZSession))"

Expected:

function function

Test CJS:

node -e "const m = require('@caterpillar-labs/zeos-link'); console.log(typeof m.default, typeof m.ZSession)"

Expected:

function function

Publishing

Before publishing:

npm run typecheck
npm test
npm run build
npm pack --dry-run

Publish:

npm publish --access public

The --access public flag matters for the first publish of a scoped npm package.

Tag release:

git tag v0.3.0
git push origin main --tags

Migration from baked-in app code

If your app currently has a local copy such as:

src/services/wallet/zSessionService.ts

replace the implementation with the package.

Before:

import ZSession from "./zSessionService";

After:

import ZSession from "@caterpillar-labs/zeos-link";

Then delete the baked-in SDK copy.

Do not change unrelated wallet paths. In particular, do not change Anchor/WharfKit/native wallet transaction flows that use:

session.transact({ actions: [...] });

ZEOS Link only supports shielded CLOAK zactions.


Minimal real-world validation checklist

After integrating into a dapp, test against the real CLOAK wallet:

1. CLOAK wallet closed -> login throws connection error
2. CLOAK wallet open but login declined -> login returns null
3. wrong chain params -> login returns null
4. correct chain params + approval -> login succeeds
5. allBalances({ ft, nftContract, atContract }) -> returns balances after wallet approval
6. balances([...], nftContract, atContract) -> same wallet request, FT filter applied in SDK
7. transact(valid zactions) -> wallet signature dialog appears
8. declined/failed transaction -> transact resolves status:error
9. successful transaction -> transact resolves status:success
10. logout -> socket closes and app state clears
11. reconnect -> login flow works again

Do not call the integration complete until these pass.


Design principles

This SDK should remain:

small
browser-first
dependency-light
protocol-focused
boring

Do not add app-specific concepts unless they are truly part of the CLOAK / ZEOS Link protocol.