{"_id":"@caterpillar-labs/zeos-link","_rev":"2-18d8632cf544e2d37e5308f137c2892a","name":"@caterpillar-labs/zeos-link","dist-tags":{"latest":"0.4.0"},"versions":{"0.3.0":{"name":"@caterpillar-labs/zeos-link","version":"0.3.0","keywords":["zeos","zeos-link","cloak","eosio","antelope","privacy","wallet","websocket","sdk"],"author":{"name":"Caterpillar Labs"},"license":"MIT","_id":"@caterpillar-labs/zeos-link@0.3.0","maintainers":[{"name":"thybow","email":"tmilville.pro@gmail.com"},{"name":"mschoenebeck","email":"matthias.schoenebeck@gmail.com"}],"homepage":"https://github.com/Caterpillar-Labs/zeos-link#readme","bugs":{"url":"https://github.com/Caterpillar-Labs/zeos-link/issues"},"dist":{"shasum":"19a9709184dd172531d41c24b7eb4cdcb2ed7ebd","tarball":"https://registry.npmjs.org/@caterpillar-labs/zeos-link/-/zeos-link-0.3.0.tgz","fileCount":15,"integrity":"sha512-bZwgYIAiJHLx5jUXrKk6NakAii1tKfQGKSqpPQgLoeVQWXitby24sj7isHXvyJnqMU3DoDx1CoqLE6DbWgGi1g==","signatures":[{"sig":"MEQCIHWsavROmT90z15Fms3BvpIlF+rrXBjIBJp4EKEFrH+OAiBouCUU6Fgt5Qg7glpFNT0IpCdSknsbKa2jfHAsf3a4+w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":295419},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.mjs","browser":"./dist/zeos-link.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"},"./global":"./dist/zeos-link.global.js","./browser":{"types":"./dist/index.d.ts","import":"./dist/zeos-link.js"},"./package.json":"./package.json"},"gitHead":"60b5d53964a00a18eee53fb5b985ac388a4e8d8d","scripts":{"test":"vitest run","build":"tsup","clean":"rm -rf dist coverage","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run clean && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"mschoenebeck","email":"matthias.schoenebeck@gmail.com"},"repository":{"url":"git+https://github.com/Caterpillar-Labs/zeos-link.git","type":"git"},"_npmVersion":"11.11.0","description":"Browser SDK for connecting web apps to the local ZEOS Link WebSocket service.","directories":{},"sideEffects":false,"_nodeVersion":"24.14.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^3.0.0","typescript":"^5.0.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/zeos-link_0.3.0_1782250923824_0.6475304522855689","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@caterpillar-labs/zeos-link","version":"0.4.0","description":"Browser SDK for connecting web apps to the local ZEOS Link WebSocket service.","type":"module","license":"MIT","author":{"name":"Caterpillar Labs"},"homepage":"https://github.com/Caterpillar-Labs/zeos-link#readme","repository":{"type":"git","url":"git+https://github.com/Caterpillar-Labs/zeos-link.git"},"bugs":{"url":"https://github.com/Caterpillar-Labs/zeos-link/issues"},"keywords":["zeos","zeos-link","cloak","eosio","antelope","privacy","wallet","websocket","sdk"],"main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.ts","browser":"./dist/zeos-link.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"},"./browser":{"types":"./dist/index.d.ts","import":"./dist/zeos-link.js"},"./global":"./dist/zeos-link.global.js","./package.json":"./package.json"},"publishConfig":{"access":"public"},"sideEffects":false,"scripts":{"clean":"rm -rf dist coverage","build":"tsup","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","prepublishOnly":"npm run clean && npm run typecheck && npm run test && npm run build"},"devDependencies":{"@types/node":"^20.0.0","tsup":"^8.0.0","typescript":"^5.0.0","vitest":"^3.0.0"},"gitHead":"06dc307a6f7236e745f154e535cb08b0757d8390","_id":"@caterpillar-labs/zeos-link@0.4.0","_nodeVersion":"24.14.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-zE4rIvh6iTn2Tsb7k5zU3NT5EzQIMid6CkVREZhAi1AbHLW2cLKGfhyxA5wY2EdvcUArMwl6b0sFjsi+3TQ12w==","shasum":"36a496d54d5263ca3ce98f79d1aed4caed33edc3","tarball":"https://registry.npmjs.org/@caterpillar-labs/zeos-link/-/zeos-link-0.4.0.tgz","fileCount":15,"unpackedSize":299988,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIB/k0OhLlC95sMoDbFFp3P0P+JFdolhJJZ0kNVotbjFyAiEA2cEUCuWwSOZgVZe1vwBwdZ9ShX7CESvy3Jq9FiqJx6s="}]},"_npmUser":{"name":"mschoenebeck","email":"matthias.schoenebeck@gmail.com"},"directories":{},"maintainers":[{"name":"thybow","email":"tmilville.pro@gmail.com"},{"name":"mschoenebeck","email":"matthias.schoenebeck@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/zeos-link_0.4.0_1786170845893_0.005423977568505212"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-23T21:42:03.649Z","modified":"2026-08-08T06:34:06.209Z","0.3.0":"2026-06-23T21:42:03.996Z","0.4.0":"2026-08-08T06:34:06.037Z"},"bugs":{"url":"https://github.com/Caterpillar-Labs/zeos-link/issues"},"author":{"name":"Caterpillar Labs"},"license":"MIT","homepage":"https://github.com/Caterpillar-Labs/zeos-link#readme","keywords":["zeos","zeos-link","cloak","eosio","antelope","privacy","wallet","websocket","sdk"],"repository":{"type":"git","url":"git+https://github.com/Caterpillar-Labs/zeos-link.git"},"description":"Browser SDK for connecting web apps to the local ZEOS Link WebSocket service.","maintainers":[{"name":"thybow","email":"tmilville.pro@gmail.com"},{"name":"mschoenebeck","email":"matthias.schoenebeck@gmail.com"}],"readme":"# @caterpillar-labs/zeos-link\n\nBrowser SDK for connecting web apps to the local **CLOAK / ZEOS Link** wallet service.\n\n`@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.\n\nThe default connection target is:\n\n```txt\nwss://127.0.0.1:9367\n```\n\nThis 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.\n\n---\n\n## TL;DR\n\nUse this package when a web app wants to support the **CLOAK wallet**.\n\n```ts\nimport ZSession, { ALL_WALLET_CONTRACTS, type ChainParams, type ZAction } from \"@caterpillar-labs/zeos-link\";\n\nconst session = new ZSession();\n\nconst chain: ChainParams = {\n  chain_id: \"aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906\",\n  protocol_contract: \"zeos4privacy\",\n  vault_contract: \"thezeosvault\",\n  alias_authority: \"thezeosalias@public\",\n};\n\nconst login = await session.login(chain);\n\nif (!login) {\n  // User declined, wallet network mismatch, or wallet rejected login.\n  return;\n}\n\nconst balances = await session.allBalances({\n  ft: true,\n  nftContract: ALL_WALLET_CONTRACTS,\n  atContract: ALL_WALLET_CONTRACTS,\n});\n\nconst zactions: ZAction[] = [\n  {\n    name: \"spend\",\n    data: {\n      contract: \"eosio.token\",\n      change_to: \"$SELF\",\n      publish_change_note: true,\n      to: [\n        {\n          to: \"alice\",\n          quantity: \"1.0000 EOS\",\n          memo: \"hello\",\n          publish_note: true,\n        },\n      ],\n    },\n  },\n];\n\nconst result = await session.transact(zactions, true, true, {\n  timeoutMs: 120_000,\n});\n\nif (result.status === \"error\") {\n  console.error(result.error);\n  return;\n}\n\nconsole.log(result);\n```\n\nImportant:\n\n* The CLOAK wallet must be running locally.\n* The wallet exposes a secure WebSocket server on `wss://127.0.0.1:9367`.\n* Login opens a native wallet approval dialog.\n* Balance requests may open a native wallet approval dialog.\n* Transactions open a native wallet signature dialog.\n* `login()` returns `null` for expected wallet rejection/decline.\n* Balance protocol errors throw.\n* `transact()` returns successful transaction responses and structured transaction error responses.\n* Network/socket/timeout failures throw.\n* This SDK only supports ZEOS/CLOAK shielded `zactions`, **not** Anchor/WharfKit `{ actions: [...] }` transactions.\n\n---\n\n## Installation\n\n```bash\nnpm install @caterpillar-labs/zeos-link\n```\n\nThe old unscoped `zeos-link` package has moved to this scoped package.\n\n---\n\n## Usage with npm / bundlers\n\n```ts\nimport ZSession from \"@caterpillar-labs/zeos-link\";\n\nconst session = new ZSession();\n```\n\nNamed import also works:\n\n```ts\nimport { ZSession } from \"@caterpillar-labs/zeos-link\";\n```\n\nImport types:\n\n```ts\nimport type {\n  ChainParams,\n  ZAction,\n  MintAction,\n  SpendAction,\n  AuthenticateAction,\n  PublishNotesAction,\n  WithdrawAction,\n  BalancesResult,\n  TransactResult,\n} from \"@caterpillar-labs/zeos-link\";\n```\n\n---\n\n## Usage as a browser ES module\n\nYou can copy the built browser file into your public assets and import it directly:\n\n```html\n<script type=\"module\">\n  import ZSession from \"/zeos-link.js\";\n\n  const session = new ZSession();\n</script>\n```\n\nWhen installed from npm, the browser ESM build is available at:\n\n```txt\nnode_modules/@caterpillar-labs/zeos-link/dist/zeos-link.js\n```\n\n---\n\n## Usage as a global browser script\n\nThe package also builds a global script for projects that do not use ESM.\n\n```html\n<script src=\"/zeos-link.global.js\"></script>\n<script>\n  const session = new ZEOSLink.ZSession();\n</script>\n```\n\nPrefer the ESM build for modern apps.\n\n---\n\n## What this SDK does\n\n`@caterpillar-labs/zeos-link` handles:\n\n* opening/reusing a WebSocket connection to the local CLOAK wallet,\n* sending request frames with unique request ids,\n* correlating wallet replies back to the pending request,\n* timing out stale requests,\n* converting expected login rejection into `null`,\n* throwing typed protocol/connection/timeout errors where appropriate,\n* routing id-less server errors such as rate-limit errors to the pending request when safe,\n* handling known wallet-server behavior such as uncorrelated transaction error frames.\n\n---\n\n## What this SDK does not do\n\nThis SDK does **not**:\n\n* manage React state,\n* store wallet sessions in localStorage,\n* choose the app's active network,\n* format balances for UI,\n* resolve token icons,\n* support Anchor/WharfKit/native EOSIO transaction shapes,\n* validate your dapp's business rules,\n* replace server-side authorization or transaction validation.\n\nKeep those responsibilities in your app.\n\n---\n\n## CLOAK wallet / ZEOS Link architecture\n\nThe CLOAK desktop wallet runs a local secure WebSocket server.\n\n```txt\nweb app\n  |\n  |  wss://127.0.0.1:9367\n  v\nCLOAK desktop wallet\n  |\n  |  native approval/signature dialogs\n  v\nZEOS wallet core / chain RPC\n```\n\nThe wallet listens on localhost only. This is intentional: the desktop wallet acts as a local signer, not as a remote public API.\n\nThe browser app sends JSON request frames. The wallet validates them, optionally shows a native Qt approval dialog, and replies with JSON response frames.\n\n---\n\n## Protocol frame shape\n\nEvery SDK request uses this shape:\n\n```json\n{\n  \"id\": 1,\n  \"request\": \"login\",\n  \"params\": {}\n}\n```\n\nNormal responses echo the request id:\n\n```json\n{\n  \"id\": 1,\n  \"status\": \"success\",\n  \"result\": {}\n}\n```\n\nError responses usually echo the request id:\n\n```json\n{\n  \"id\": 1,\n  \"status\": \"error\",\n  \"error\": \"not logged in\"\n}\n```\n\nSome 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.\n\n---\n\n## Supported protocol requests\n\nThe CLOAK wallet currently supports these request names:\n\n```txt\nlogin\nall_balances\nbalances\ntransact\n```\n\nUnknown requests receive:\n\n```json\n{\n  \"status\": \"error\",\n  \"error\": \"unknown request\"\n}\n```\n\n---\n\n## Public API\n\n```ts\nexport class ZSession {\n  constructor(url?: string, options?: SessionOptions);\n\n  login(chain: ChainParams, onClose?: () => void): Promise<LoginResult | null>;\n  logout(): void;\n\n  isConnected(): boolean;\n  handle(): string | null;\n\n  allBalances(\n    ft?: boolean,\n    nft?: boolean,\n    at?: boolean,\n    opts?: RequestOptions\n  ): Promise<BalancesResult>;\n\n  balances(\n    ftSymbols?: string[],\n    nftContract?: string,\n    atContract?: string,\n    opts?: RequestOptions\n  ): Promise<BalancesResult>;\n\n  transact(\n    zactions: ZAction[],\n    addFee?: boolean,\n    publishFeeNote?: boolean,\n    opts?: RequestOptions\n  ): Promise<TransactResult>;\n}\n\nexport default ZSession;\n```\n\n---\n\n## Login\n\n### API\n\n```ts\nconst result = await session.login(chain, onClose);\n```\n\n### Type\n\n```ts\nlogin(\n  chain: ChainParams,\n  onClose?: () => void\n): Promise<LoginResult | null>\n```\n\n### Chain params\n\n```ts\ninterface ChainParams {\n  chain_id: string;\n  protocol_contract: string;\n  vault_contract: string;\n  alias_authority: string;\n}\n```\n\nExample:\n\n```ts\nconst login = await session.login({\n  chain_id: \"aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906\",\n  protocol_contract: \"zeos4privacy\",\n  vault_contract: \"thezeosvault\",\n  alias_authority: \"thezeosalias@public\",\n});\n```\n\n### Login behavior\n\nThe wallet validates that the provided login fields match the wallet's currently active network configuration.\n\nThe wallet checks:\n\n```txt\nchain_id\nprotocol_contract\nvault_contract\nalias_authority\n```\n\n`alias_authority` is expected in this form:\n\n```txt\naccount@permission\n```\n\nExample:\n\n```txt\nthezeosalias@public\n```\n\nIf the params do not match the wallet's active network, the wallet rejects the login.\n\nIf the params match, the wallet opens a native login approval dialog. The user must accept the request in the CLOAK wallet.\n\n### Login result\n\nOn approval:\n\n```ts\nconst login = await session.login(chain);\n\nif (login) {\n  console.log(\"Connected\", login.result);\n}\n```\n\nOn expected rejection:\n\n```ts\nconst login = await session.login(chain);\n\nif (!login) {\n  // User declined, wallet rejected login, or active wallet network did not match.\n}\n```\n\n`login()` returns `null` for expected wallet-level rejection. It throws only for transport/runtime failures such as connection errors, malformed replies, or timeouts.\n\n### Important: do not treat the login result as a public account\n\nThe 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.\n\n---\n\n## Logout\n\n```ts\nsession.logout();\n```\n\nThis closes the WebSocket connection, clears the locally stored chain params and handle, and rejects pending requests.\n\nIt does not alter wallet state inside the desktop wallet.\n\n---\n\n## Connection status\n\n```ts\nsession.isConnected();\n```\n\nReturns whether the underlying WebSocket is currently open.\n\n```ts\nsession.handle();\n```\n\nReturns the current wallet handle from the login response, or `null`.\n\nAgain: the handle is not a public EOSIO account identity.\n\n---\n\n## Query all balances\n\n### API\n\n```ts\nimport { ALL_WALLET_CONTRACTS } from \"@caterpillar-labs/zeos-link\";\n\nconst balances = await session.allBalances({\n  ft: true,\n  nftContract: ALL_WALLET_CONTRACTS,\n  atContract: \"thezeosalias\",\n});\n```\n\n### Type\n\n```ts\nallBalances(\n  params?: AllBalancesParams,\n  opts?: RequestOptions\n): Promise<BalancesResult>\n\ninterface AllBalancesParams {\n  ft?: boolean;\n  nftContract?: string;\n  atContract?: string;\n}\n```\n\n### Request params\n\nThe SDK sends the wire shape supported by the CLOAK desktop wallet:\n\n```json\n{\n  \"request\": \"all_balances\",\n  \"params\": {\n    \"ft\": true,\n    \"nft_contract\": \"\",\n    \"at_contract\": \"thezeosalias\"\n  }\n}\n```\n\nMeaning:\n\n```txt\nft            = include fungible token balances (`fts`)\nnft_contract  = NFT contract filter; `\"\"` means all contracts (wallet contract id 0)\nat_contract   = auth-token contract filter; `\"\"` means all contracts (wallet contract id 0)\n```\n\nOmit `nftContract` / `atContract` to skip those sections entirely.\n\n### Result shape\n\nTypical result:\n\n```ts\ninterface BalancesResult {\n  fts?: string[];\n  nfts?: unknown[] | string[];\n  ats?: {\n    spent: string[];\n    unspent: string[];\n  } | string[];\n}\n```\n\nExample:\n\n```ts\nconst balances = await session.allBalances({ ft: true });\n\nconsole.log(balances.fts);\n```\n\n### User approval\n\nThe wallet may show a native balance-request approval dialog. Do not assume this is a silent background query.\n\n---\n\n## Query filtered balances\n\n### API\n\n```ts\nconst balances = await session.balances(\n  [\"4,EOS\", \"8,CLOAK\"],\n  \"atomicassets\",\n  \"theauthcontr\",\n);\n```\n\n### Type\n\n```ts\nbalances(\n  ftSymbols?: string[],\n  nftContract?: string,\n  atContract?: string,\n  opts?: RequestOptions\n): Promise<BalancesResult>\n```\n\n### Request params\n\n`balances()` calls `all_balances` on the wallet and filters `fts` client-side when `ftSymbols` is provided:\n\n```json\n{\n  \"request\": \"all_balances\",\n  \"params\": {\n    \"ft\": true,\n    \"nft_contract\": \"atomicassets\",\n    \"at_contract\": \"theauthcontr\"\n  }\n}\n```\n\nThe desktop wallet does not expose a separate `balances` request.\n\n### Fungible token symbol format\n\nUse EOSIO-style symbol strings:\n\n```txt\nprecision,SYMBOL\n```\n\nExamples:\n\n```txt\n4,EOS\n8,CLOAK\n3,UN\n```\n\nWhen a requested fungible token balance is not found, the wallet may return a zero balance for that symbol.\n\n---\n\n## Transact\n\n### API\n\n```ts\nconst result = await session.transact(zactions);\n```\n\n### Type\n\n```ts\ntransact(\n  zactions: ZAction[],\n  addFee?: boolean,\n  publishFeeNote?: boolean,\n  opts?: RequestOptions\n): Promise<TransactResult>\n```\n\n### Defaults\n\n```txt\naddFee         true\npublishFeeNote true\ntimeoutMs      60000\n```\n\nFor proof-heavy flows, use a longer timeout:\n\n```ts\nconst result = await session.transact(zactions, true, true, {\n  timeoutMs: 120_000,\n});\n```\n\nThe SDK sends a `transact` request with the active login chain params:\n\n```json\n{\n  \"request\": \"transact\",\n  \"params\": {\n    \"chain_id\": \"...\",\n    \"protocol_contract\": \"...\",\n    \"vault_contract\": \"...\",\n    \"alias_authority\": \"...\",\n    \"add_fee\": true,\n    \"publish_fee_note\": true,\n    \"zactions\": []\n  }\n}\n```\n\n### Exact wallet-resolved transaction fee\n\nCompatible CLOAK wallets include the exact fee actually resolved and paid by the private wallet in successful transaction responses:\n\n```ts\nconst result = await session.transact(zactions);\n\nif (result.status === \"success\" && result.payload?.tx_fee) {\n  console.log(result.payload.tx_fee);\n  // Example: \"0.0123 CLOAK@thezeostoken\"\n}\n```\n\n`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.\n\n### Important: this is not Anchor / WharfKit\n\nThis is valid for CLOAK / ZEOS Link:\n\n```ts\nawait session.transact([\n  {\n    name: \"spend\",\n    data: {\n      contract: \"eosio.token\",\n      change_to: \"$SELF\",\n      publish_change_note: true,\n      to: [\n        {\n          to: \"alice\",\n          quantity: \"1.0000 EOS\",\n          memo: \"\",\n          publish_note: true,\n        },\n      ],\n    },\n  },\n]);\n```\n\nThis is **not** a ZEOS Link transaction:\n\n```ts\nawait session.transact({\n  actions: [\n    {\n      account: \"eosio.token\",\n      name: \"transfer\",\n      data: {},\n    },\n  ],\n});\n```\n\n`{ actions: [...] }` belongs to Anchor, WharfKit, or other native EOSIO wallet/session APIs. Do not mix those shapes into this SDK.\n\n---\n\n## ZActions guide\n\n`zactions` are the high-level private actions sent to the CLOAK wallet through:\n\n```ts\nawait session.transact(zactions);\n```\n\nThe public TypeScript type is:\n\n```ts\ntype ZAction =\n  | MintAction\n  | SpendAction\n  | AuthenticateAction\n  | PublishNotesAction\n  | WithdrawAction;\n```\n\nThe 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.\n\nThe supported action names are:\n\n```txt\nmint\nspend\nauthenticate\npublishnotes\nwithdraw\n```\n\n---\n\n## Common string formats\n\n```txt\nEOSIO account/name:\n  \"eosio.token\"\n  \"atomicassets\"\n  \"mycontract\"\n\nAuthorization:\n  \"actor@permission\"\n  \"mycontract@active\"\n\nFT quantity:\n  \"10.0000 EOS\"\n\nNFT quantity:\n  \"123456789\"\n\nSymbol filter:\n  \"4,EOS\"\n\nShielded address:\n  \"za1...\"\n\nSelf placeholder:\n  \"$SELF\"\n\nAuth token placeholder:\n  \"$AUTH0\" ... \"$AUTH9\"\n\nExisting auth token commitment:\n  64-char hex string\n```\n\nNFTs 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:\n\n```txt\n\"123456789\"\n```\n\n---\n\n## Placeholders\n\n`$SELF` means the current wallet's default shielded address.\n\n`$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.\n\nMemos may also contain:\n\n```txt\n$SELF\n$AUTH0 ... $AUTH9\n```\n\nThe wallet resolves those placeholders during transaction construction.\n\n---\n\n## `mint`\n\nCreates a new shielded note.\n\n```ts\nconst zactions: ZAction[] = [\n  {\n    name: \"mint\",\n    data: {\n      to: \"$SELF\",\n      contract: \"eosio.token\",\n      quantity: \"10.0000 EOS\",\n      memo: \"\",\n      from: \"alice\",\n      publish_note: true,\n    },\n  },\n];\n```\n\nType shape:\n\n```ts\ninterface MintAction {\n  name: \"mint\";\n  data: {\n    to: string;\n    contract: string;\n    quantity: string;\n    memo: string;\n    from: string;\n    publish_note: boolean;\n  };\n}\n```\n\nField notes:\n\n```txt\nto:\n  \"$SELF\" or shielded address\n\ncontract:\n  token/NFT/auth-token contract account\n\nquantity:\n  FT: \"10.0000 EOS\"\n  NFT: \"123456789\"\n  auth-token mint: \"0\"\n\nfrom:\n  EOSIO account that funded the protocol asset buffer\n\npublish_note:\n  whether the encrypted note should be published for recipient discovery\n```\n\nAuth token minting is a special case of `mint` where `quantity` is `\"0\"`.\n\nFor 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.\n\n---\n\n## `spend`\n\nSpends existing shielded notes to shielded recipients, unshielded EOSIO accounts, or both.\n\n```ts\nconst zactions: ZAction[] = [\n  {\n    name: \"spend\",\n    data: {\n      contract: \"eosio.token\",\n      change_to: \"$SELF\",\n      publish_change_note: true,\n      to: [\n        {\n          to: \"bob\",\n          quantity: \"5.0000 EOS\",\n          memo: \"public output\",\n          publish_note: true,\n        },\n        {\n          to: \"za1...\",\n          quantity: \"2.0000 EOS\",\n          memo: \"shielded output\",\n          publish_note: true,\n        },\n      ],\n    },\n  },\n];\n```\n\nType shape:\n\n```ts\ninterface SpendAction {\n  name: \"spend\";\n  data: {\n    contract: string;\n    change_to: string;\n    publish_change_note: boolean;\n    to: Array<{\n      to: string;\n      quantity: string;\n      memo: string;\n      publish_note: boolean;\n    }>;\n  };\n}\n```\n\nRecipient rules:\n\n```txt\n\"$SELF\":\n  current wallet default shielded address\n\n\"za1...\":\n  shielded address\n\n<=12-char EOSIO name:\n  unshielded EOSIO account recipient\n\n64-char hex string:\n  auth/vault recipient hash\n```\n\nFT spend quantity example:\n\n```txt\n\"10.0000 EOS\"\n```\n\nNFT spend quantity example:\n\n```txt\n\"123456789\"\n```\n\n---\n\n## `authenticate`\n\nPrivately authorizes EOSIO actions using an auth token.\n\nThis 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.\n\n```ts\nconst zactions: ZAction[] = [\n  {\n    name: \"authenticate\",\n    data: {\n      auth_token: \"$AUTH0\",\n      burn: true,\n      actions: [\n        {\n          account: \"mycontract\",\n          name: \"claimauctiop\",\n          authorization: [\"mycontract@active\"],\n          data: {\n            round: 7,\n          },\n        },\n      ],\n    },\n  },\n];\n```\n\nType shape:\n\n```ts\ninterface AuthenticateAction {\n  name: \"authenticate\";\n  data: {\n    auth_token: string;\n    burn: boolean;\n    actions: Array<{\n      account: string;\n      name: string;\n      authorization: string[];\n      data: Record<string, unknown>;\n    }>;\n  };\n}\n```\n\nImportant: `actions[].data` is normal unpacked EOSIO JSON action data, exactly like native EOSIO wallets accept.\n\nDo **not** pass packed hex here.\n\nGood:\n\n```ts\ndata: { round: 7 }\n```\n\nBad:\n\n```ts\ndata: \"deadbeef\"\n```\n\nThe CLOAK wallet packs this JSON action data internally using the chain ABI before the Rust transaction resolver receives it.\n\n### Auth token references\n\n`auth_token` may be:\n\n```txt\n\"$AUTH0\" ... \"$AUTH9\"\n```\n\nfor auth tokens minted earlier in the same transaction, or:\n\n```txt\n64-char hex commitment\n```\n\nfor an existing unspent auth token.\n\n### Private dapp action pattern\n\nA dapp can expose a public action and a private/authenticated variant.\n\nExample:\n\n```cpp\nACTION claimauction(const eosio::name& owner, const uint32_t& round);\nACTION claimauctiop(const uint32_t& round);\nZAUTHENTICATE(ZACTION(claimauctiop))\n```\n\nThe private frontend sends:\n\n```ts\nconst zactions: ZAction[] = [\n  {\n    name: \"authenticate\",\n    data: {\n      auth_token: String(authTokenCommitment),\n      burn: true,\n      actions: [\n        {\n          account: \"mycontract\",\n          name: \"claimauctiop\",\n          authorization: [\"mycontract@active\"],\n          data: { round },\n        },\n      ],\n    },\n  },\n];\n```\n\nThe protocol verifies the auth proof, then notifies the authenticated contract. The dapp contract reads the authenticated action buffer and executes allowed private actions.\n\n### Troubleshooting `authenticate`\n\n`authenticate` can fail if:\n\n```txt\n- auth_token is invalid, spent, or unavailable\n- burn is wrong for the intended flow\n- nested account/name is wrong\n- nested action is not in the dapp contract ABI\n- nested data does not match the ABI\n- chain RPC cannot fetch ABI / pack action data\n- the dapp contract does not allow the private action in its authenticate handler\n```\n\n---\n\n## `publishnotes`\n\nPublishes encrypted note ciphertexts.\n\n```ts\nconst zactions: ZAction[] = [\n  {\n    name: \"publishnotes\",\n    data: {\n      notes: [\"...base64-note-ciphertext...\"],\n    },\n  },\n];\n```\n\nType shape:\n\n```ts\ninterface PublishNotesAction {\n  name: \"publishnotes\";\n  data: {\n    notes: string[];\n  };\n}\n```\n\nMost frontend apps should not invent these strings manually. They usually come from wallet/protocol flows.\n\n---\n\n## `withdraw`\n\nDrains assets from the shielded protocol contract's asset buffer to an unshielded EOSIO account.\n\nThis 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.\n\n```ts\nconst zactions: ZAction[] = [\n  {\n    name: \"withdraw\",\n    data: {\n      contract: \"eosio.token\",\n      quantity: \"10.0000 EOS\",\n      memo: \"settlement\",\n      to: \"alice\",\n    },\n  },\n];\n```\n\nType shape:\n\n```ts\ninterface WithdrawAction {\n  name: \"withdraw\";\n  data: {\n    contract: string;\n    quantity: string;\n    memo: string;\n    to: string;\n  };\n}\n```\n\nFT quantity example:\n\n```txt\n\"10.0000 EOS\"\n```\n\nNFT quantity example:\n\n```txt\n\"123456789\"\n```\n\nThe protocol checks the asset buffer, matches the requested contract/symbol/value, and sends the asset out from the protocol contract to `to`.\n\n---\n\n## Request options\n\nMost request methods accept:\n\n```ts\ninterface RequestOptions {\n  timeoutMs?: number;\n}\n```\n\nExamples:\n\n```ts\nawait session.allBalances(\n  { ft: true, nftContract: ALL_WALLET_CONTRACTS, atContract: ALL_WALLET_CONTRACTS },\n  { timeoutMs: 30_000 },\n);\n\nawait session.transact(zactions, true, true, {\n  timeoutMs: 120_000,\n});\n```\n\nRecommended defaults:\n\n```txt\nlogin        30s fixed internally\nbalances     15s\ntransact     60s or longer for proof-generation-heavy flows\n```\n\nTransaction signing can be slow because the wallet may need to resolve, prove, sign, and publish a shielded transaction.\n\n---\n\n## Error contract\n\nThis is the most important API contract.\n\n### Login\n\n```txt\nlogin approved\n  -> resolves LoginResult\n\nuser declined login\n  -> resolves null\n\nwallet network mismatch\n  -> resolves null\n\nwallet rejects login params\n  -> resolves null\n\nsocket/network/timeout/runtime failure\n  -> throws\n```\n\nUse:\n\n```ts\ntry {\n  const login = await session.login(chain);\n\n  if (!login) {\n    // Expected wallet-level rejection.\n    return;\n  }\n\n  // Connected.\n} catch (err) {\n  // Transport/runtime failure.\n}\n```\n\n### Balances\n\n```txt\nbalance request approved\n  -> resolves BalancesResult\n\nwallet protocol error\n  -> throws ProtocolError\n\nrate limited / message too large\n  -> throws protocol-style error\n\nsocket/network/timeout/runtime failure\n  -> throws\n```\n\nUse:\n\n```ts\ntry {\n  const balances = await session.allBalances({\n  ft: true,\n  nftContract: ALL_WALLET_CONTRACTS,\n  atContract: ALL_WALLET_CONTRACTS,\n});\n} catch (err) {\n  // Show a real error. Do not treat as \"no balances\".\n}\n```\n\n### Transact\n\n```txt\ntransaction approved and processed\n  -> resolves TransactResult with status: \"success\"\n\ntransaction rejected/failed at wallet/protocol level\n  -> resolves TransactResult with status: \"error\"\n\nuncorrelated transaction error frame from wallet\n  -> resolves structured transaction error result\n\nsocket/network/timeout/runtime failure\n  -> throws\n```\n\nUse:\n\n```ts\ntry {\n  const result = await session.transact(zactions);\n\n  if (result.status === \"error\") {\n    // Wallet/protocol-level transaction failure.\n    console.error(result.error);\n    return;\n  }\n\n  // Success.\n} catch (err) {\n  // Transport/runtime failure.\n}\n```\n\nWhy transaction errors resolve instead of throw:\n\nA 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`.\n\n---\n\n## Error handling pattern\n\nRecommended app-side helper:\n\n```ts\nimport {\n  ProtocolError,\n  TimeoutError,\n  ConnectionError,\n  SendError,\n} from \"@caterpillar-labs/zeos-link\";\n\nfunction describeCloakError(err: unknown): string {\n  if (err instanceof TimeoutError) {\n    return \"The CLOAK wallet did not respond in time.\";\n  }\n\n  if (err instanceof ConnectionError) {\n    return \"Could not connect to the local CLOAK wallet.\";\n  }\n\n  if (err instanceof ProtocolError) {\n    return err.message || \"The CLOAK wallet rejected the request.\";\n  }\n\n  if (err instanceof SendError) {\n    return err.message || \"Could not send the request to the CLOAK wallet.\";\n  }\n\n  if (err instanceof Error) {\n    return err.message;\n  }\n\n  return \"Unknown CLOAK wallet error.\";\n}\n```\n\nThen:\n\n```ts\ntry {\n  const balances = await session.allBalances({\n  ft: true,\n  nftContract: ALL_WALLET_CONTRACTS,\n  atContract: ALL_WALLET_CONTRACTS,\n});\n} catch (err) {\n  notifyUser(describeCloakError(err));\n}\n```\n\n---\n\n## Detecting whether CLOAK wallet is available\n\nThe simplest check is attempting login.\n\n```ts\nconst session = new ZSession();\n\ntry {\n  const login = await session.login(chain);\n\n  if (!login) {\n    console.log(\"Wallet rejected login or user declined.\");\n  }\n} catch (err) {\n  console.log(\"CLOAK wallet is unavailable or unreachable.\");\n}\n```\n\nCommon reasons connection fails:\n\n```txt\n- CLOAK desktop wallet is not running\n- no wallet is open inside CLOAK\n- local WSS server is not listening\n- browser rejected the local TLS certificate\n- browser/app origin is blocked by future wallet origin policy\n- local firewall/proxy/security software interferes with localhost WSS\n```\n\n---\n\n## React integration pattern\n\nKeep the SDK instance in app wallet state, not inside random components.\n\nExample sketch:\n\n```ts\nimport ZSession from \"@caterpillar-labs/zeos-link\";\n\nconst session = new ZSession();\n\nconst login = await session.login(chain, () => {\n  // Wallet socket closed.\n  // Clear app wallet state here.\n});\n\nif (!login) {\n  // User declined or wallet rejected.\n  return;\n}\n\n// Store session as the active CLOAK wallet session.\nwalletSessionRef.current = session;\nwalletTypeRef.current = \"CLOAK\";\n```\n\nAfter transaction:\n\n```ts\nconst result = await session.transact(zactions, true, true, {\n  timeoutMs: 120_000,\n});\n\nif (result.status === \"error\") {\n  // Show transaction error.\n  return;\n}\n\n// Refresh balances / local app state.\n```\n\nDo not expose `ZSession` internals to UI components. Wrap it in your app's wallet adapter.\n\n---\n\n## Suggested app adapter boundary\n\nGood:\n\n```txt\napp wallet adapter\n  - knows about React state\n  - knows selected network\n  - knows token icons\n  - knows notifications\n  - owns walletSessionRef\n  - imports ZSession from @caterpillar-labs/zeos-link\n```\n\nBad:\n\n```txt\nZEOS Link SDK\n  - imports React app types\n  - knows about token icons\n  - knows about app notifications\n  - supports Anchor/WharfKit transaction shapes\n```\n\nKeep the SDK boring and protocol-focused.\n\n---\n\n## Security notes\n\n### Localhost only\n\nThe CLOAK wallet is expected to listen on localhost:\n\n```txt\nwss://127.0.0.1:9367\n```\n\nDo not expose the wallet WSS server on a public network interface.\n\n### Validate chain params in the app\n\nThe SDK validates basic string shape. Your app is still responsible for choosing the correct network config.\n\nWrong chain params should fail login, but do not rely on wallet rejection as your only safety layer.\n\n### Treat wallet dialogs as the security boundary\n\nLogin, balance reads, and transactions can show native wallet dialogs. Design UX around that.\n\nDo not spam wallet prompts.\n\n### Do not assume login means public account identity\n\nCLOAK is a privacy wallet. The login result is an opaque wallet handle, not a public account proof.\n\n### Pin CDN versions\n\nIf loading from a CDN, pin exact versions.\n\nGood:\n\n```html\n<script type=\"module\">\n  import ZSession from \"https://unpkg.com/@caterpillar-labs/zeos-link@0.3.0/dist/zeos-link.js\";\n</script>\n```\n\nBad:\n\n```html\n<script type=\"module\">\n  import ZSession from \"https://unpkg.com/@caterpillar-labs/zeos-link@latest/dist/zeos-link.js\";\n</script>\n```\n\n### Never auto-submit sensitive transactions\n\nAlways 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()`.\n\n---\n\n## Raw protocol examples\n\nYou normally do not need this when using the SDK, but it is useful for debugging and for AI agents reading the repo.\n\n### Login request\n\n```json\n{\n  \"id\": 1,\n  \"request\": \"login\",\n  \"params\": {\n    \"chain_id\": \"aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906\",\n    \"protocol_contract\": \"zeos4privacy\",\n    \"vault_contract\": \"thezeosvault\",\n    \"alias_authority\": \"thezeosalias@public\"\n  }\n}\n```\n\n### Login success\n\n```json\n{\n  \"id\": 1,\n  \"status\": \"success\",\n  \"result\": \"anonymous\"\n}\n```\n\n### Login rejection\n\n```json\n{\n  \"id\": 1,\n  \"status\": \"error\",\n  \"error\": \"declined\"\n}\n```\n\nor:\n\n```json\n{\n  \"id\": 1,\n  \"status\": \"error\",\n  \"error\": \"login declined\"\n}\n```\n\n### All balances request\n\n```json\n{\n  \"id\": 2,\n  \"request\": \"all_balances\",\n  \"params\": {\n    \"ft\": true,\n    \"nft_contract\": \"\",\n    \"at_contract\": \"thezeosalias\"\n  }\n}\n```\n\n### Filtered balances (`balances()` helper)\n\n`balances()` uses `all_balances` and filters `fts` in the SDK:\n\n```json\n{\n  \"id\": 3,\n  \"request\": \"all_balances\",\n  \"params\": {\n    \"ft\": true,\n    \"nft_contract\": \"atomicassets\",\n    \"at_contract\": \"theauthcontr\"\n  }\n}\n```\n\n### Transact request\n\n```json\n{\n  \"id\": 4,\n  \"request\": \"transact\",\n  \"params\": {\n    \"chain_id\": \"...\",\n    \"protocol_contract\": \"...\",\n    \"vault_contract\": \"...\",\n    \"alias_authority\": \"...\",\n    \"add_fee\": true,\n    \"publish_fee_note\": true,\n    \"zactions\": [\n      {\n        \"name\": \"spend\",\n        \"data\": {\n          \"contract\": \"eosio.token\",\n          \"change_to\": \"$SELF\",\n          \"publish_change_note\": true,\n          \"to\": [\n            {\n              \"to\": \"alice\",\n              \"quantity\": \"1.0000 EOS\",\n              \"memo\": \"\",\n              \"publish_note\": true\n            }\n          ]\n        }\n      }\n    ]\n  }\n}\n```\n\n### Error response\n\n```json\n{\n  \"id\": 4,\n  \"status\": \"error\",\n  \"error\": \"not logged in\"\n}\n```\n\n### Id-less low-level error\n\n```json\n{\n  \"status\": \"error\",\n  \"error\": \"rate limited\"\n}\n```\n\nThe SDK handles this if exactly one request is pending.\n\n---\n\n## Development\n\nInstall dependencies:\n\n```bash\nnpm install\n```\n\nTypecheck:\n\n```bash\nnpm run typecheck\n```\n\nRun tests:\n\n```bash\nnpm test\n```\n\nBuild:\n\n```bash\nnpm run build\n```\n\nThe build outputs:\n\n```txt\ndist/index.mjs\ndist/index.cjs\ndist/index.d.ts\ndist/index.d.cts\ndist/zeos-link.js\ndist/zeos-link.min.js\ndist/zeos-link.global.js\n```\n\n---\n\n## Smoke test package exports\n\nFrom outside the repo:\n\n```bash\nmkdir /tmp/zeos-link-smoke\ncd /tmp/zeos-link-smoke\nnpm init -y\nnpm install @caterpillar-labs/zeos-link\n```\n\nTest ESM:\n\n```bash\nnode -e \"import('@caterpillar-labs/zeos-link').then(m => console.log(typeof m.default, typeof m.ZSession))\"\n```\n\nExpected:\n\n```txt\nfunction function\n```\n\nTest CJS:\n\n```bash\nnode -e \"const m = require('@caterpillar-labs/zeos-link'); console.log(typeof m.default, typeof m.ZSession)\"\n```\n\nExpected:\n\n```txt\nfunction function\n```\n\n---\n\n## Publishing\n\nBefore publishing:\n\n```bash\nnpm run typecheck\nnpm test\nnpm run build\nnpm pack --dry-run\n```\n\nPublish:\n\n```bash\nnpm publish --access public\n```\n\nThe `--access public` flag matters for the first publish of a scoped npm package.\n\nTag release:\n\n```bash\ngit tag v0.3.0\ngit push origin main --tags\n```\n\n---\n\n## Migration from baked-in app code\n\nIf your app currently has a local copy such as:\n\n```txt\nsrc/services/wallet/zSessionService.ts\n```\n\nreplace the implementation with the package.\n\nBefore:\n\n```ts\nimport ZSession from \"./zSessionService\";\n```\n\nAfter:\n\n```ts\nimport ZSession from \"@caterpillar-labs/zeos-link\";\n```\n\nThen delete the baked-in SDK copy.\n\nDo not change unrelated wallet paths. In particular, do not change Anchor/WharfKit/native wallet transaction flows that use:\n\n```ts\nsession.transact({ actions: [...] });\n```\n\nZEOS Link only supports shielded CLOAK `zactions`.\n\n---\n\n## Minimal real-world validation checklist\n\nAfter integrating into a dapp, test against the real CLOAK wallet:\n\n```txt\n1. CLOAK wallet closed -> login throws connection error\n2. CLOAK wallet open but login declined -> login returns null\n3. wrong chain params -> login returns null\n4. correct chain params + approval -> login succeeds\n5. allBalances({ ft, nftContract, atContract }) -> returns balances after wallet approval\n6. balances([...], nftContract, atContract) -> same wallet request, FT filter applied in SDK\n7. transact(valid zactions) -> wallet signature dialog appears\n8. declined/failed transaction -> transact resolves status:error\n9. successful transaction -> transact resolves status:success\n10. logout -> socket closes and app state clears\n11. reconnect -> login flow works again\n```\n\nDo not call the integration complete until these pass.\n\n---\n\n## Design principles\n\nThis SDK should remain:\n\n```txt\nsmall\nbrowser-first\ndependency-light\nprotocol-focused\nboring\n```\n\nDo not add app-specific concepts unless they are truly part of the CLOAK / ZEOS Link protocol.\n","readmeFilename":"README.md"}