@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:9367This 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()returnsnullfor 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-linkThe 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.jsUsage 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 RPCThe 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
transactUnknown 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_authorityalias_authority is expected in this form:
account@permissionExample:
thezeosalias@publicIf 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,SYMBOLExamples:
4,EOS
8,CLOAK
3,UNWhen 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 60000For 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
withdrawCommon 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 stringNFTs 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 ... $AUTH9The 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 discoveryAuth 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 hashFT 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 commitmentfor 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 handlerpublishnotes
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 flowsTransaction 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
-> throwsUse:
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
-> throwsUse:
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
-> throwsUse:
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 WSSReact 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-linkBad:
ZEOS Link SDK
- imports React app types
- knows about token icons
- knows about app notifications
- supports Anchor/WharfKit transaction shapesKeep the SDK boring and protocol-focused.
Security notes
Localhost only
The CLOAK wallet is expected to listen on localhost:
wss://127.0.0.1:9367Do 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 installTypecheck:
npm run typecheckRun tests:
npm testBuild:
npm run buildThe 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.jsSmoke 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-linkTest ESM:
node -e "import('@caterpillar-labs/zeos-link').then(m => console.log(typeof m.default, typeof m.ZSession))"Expected:
function functionTest CJS:
node -e "const m = require('@caterpillar-labs/zeos-link'); console.log(typeof m.default, typeof m.ZSession)"Expected:
function functionPublishing
Before publishing:
npm run typecheck
npm test
npm run build
npm pack --dry-runPublish:
npm publish --access publicThe --access public flag matters for the first publish of a scoped npm package.
Tag release:
git tag v0.3.0
git push origin main --tagsMigration from baked-in app code
If your app currently has a local copy such as:
src/services/wallet/zSessionService.tsreplace 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 againDo not call the integration complete until these pass.
Design principles
This SDK should remain:
small
browser-first
dependency-light
protocol-focused
boringDo not add app-specific concepts unless they are truly part of the CLOAK / ZEOS Link protocol.
