@kairoguard/sdk 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/backend.d.ts CHANGED
@@ -253,6 +253,7 @@ export declare class BackendClient {
253
253
  private apiKey;
254
254
  constructor(opts: BackendClientOpts);
255
255
  setApiKey(key: string): void;
256
+ getBaseUrl(): string;
256
257
  private request;
257
258
  getHealth(): Promise<HealthResponse>;
258
259
  register(label: string, email?: string): Promise<RegisterKeyResponse>;
package/dist/backend.js CHANGED
@@ -15,6 +15,9 @@ export class BackendClient {
15
15
  setApiKey(key) {
16
16
  this.apiKey = key;
17
17
  }
18
+ getBaseUrl() {
19
+ return this.baseUrl;
20
+ }
18
21
  async request(method, path, body) {
19
22
  const headers = { "Content-Type": "application/json" };
20
23
  if (this.apiKey) {
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { verifyAuditBundle } from "./auditBundle.js";
6
6
  import { BackendClient } from "./backend.js";
7
+ import { KairoClient } from "./client.js";
7
8
  import { SKILL_MD, API_REFERENCE_MD, SDK_REFERENCE_MD } from "./skill-templates.js";
8
9
  const CONFIG_DIR = join(homedir(), ".kairo");
9
10
  const CONFIG_PATH = join(CONFIG_DIR, "config.json");
@@ -80,6 +81,26 @@ async function cmdHealth() {
80
81
  const res = await client.getHealth();
81
82
  console.log(JSON.stringify(res, null, 2));
82
83
  }
84
+ async function cmdWalletCreate(args) {
85
+ const curveRaw = flag(args, "--curve") ?? "secp256k1";
86
+ if (curveRaw !== "secp256k1" && curveRaw !== "ed25519") {
87
+ console.error('Invalid --curve value. Use "secp256k1" or "ed25519".');
88
+ process.exit(1);
89
+ }
90
+ const policyId = flag(args, "--policy-id");
91
+ const stableId = flag(args, "--stable-id");
92
+ const cfg = requireConfig();
93
+ const kairo = new KairoClient({
94
+ apiKey: cfg.apiKey,
95
+ backendUrl: cfg.backendUrl,
96
+ });
97
+ const wallet = await kairo.createWallet({
98
+ curve: curveRaw,
99
+ policyObjectId: policyId,
100
+ stableId,
101
+ });
102
+ console.log(JSON.stringify(wallet, null, 2));
103
+ }
83
104
  async function cmdRegister(args) {
84
105
  const label = requireFlag(args, "--label", "name");
85
106
  const client = getClient();
@@ -184,6 +205,8 @@ Setup:
184
205
 
185
206
  Wallet & Policy:
186
207
  health Server health check
208
+ wallet-create [--curve secp256k1|ed25519] [--policy-id <id>] [--stable-id <id>]
209
+ Create a new dWallet via SDK DKG flow
187
210
  register --label <name> Register new API key
188
211
  policy-create --stable-id <id> --allow <addrs> Create policy
189
212
  policy-register --policy-id <id> Register policy version
@@ -206,6 +229,8 @@ async function main() {
206
229
  return cmdInit(rest);
207
230
  case "health":
208
231
  return cmdHealth();
232
+ case "wallet-create":
233
+ return cmdWalletCreate(rest);
209
234
  case "register":
210
235
  return cmdRegister(rest);
211
236
  case "policy-create":
package/dist/client.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface KairoClientOpts {
17
17
  backendUrl?: string;
18
18
  /** Local directory for secret share storage. Defaults to ~/.kairo/keys */
19
19
  storePath?: string;
20
- /** Sui RPC URL for fetching Ika protocol params. Defaults to public testnet. */
20
+ /** Sui RPC URL for fetching Ika protocol params. Defaults to backend's proxy, then SUI_RPC_URL env, then public testnet. */
21
21
  suiRpcUrl?: string;
22
22
  /** Sui network. Defaults to "testnet". */
23
23
  network?: "testnet" | "mainnet";
@@ -138,6 +138,10 @@ export declare class KairoClient {
138
138
  private network;
139
139
  private evmRpcUrls;
140
140
  constructor(opts: KairoClientOpts);
141
+ /**
142
+ * Resolve Sui RPC URL, testing backend proxy first and falling back to public RPC.
143
+ */
144
+ private resolveSuiRpcUrl;
141
145
  /**
142
146
  * Create a new dWallet. Runs DKG on the agent's machine, submits to backend,
143
147
  * and optionally provisions the wallet in the vault.
package/dist/client.js CHANGED
@@ -14,7 +14,43 @@ import { Curve, fetchProtocolParams, deriveEncryptionKeys, generateSeed, generat
14
14
  import { Hash, SignatureAlgorithm, createUserSignMessageWithPublicOutput } from "@ika.xyz/sdk";
15
15
  import { computeEvmIntentFromUnsignedTxBytes } from "./evmIntent.js";
16
16
  import { keccak256, recoverTransactionAddress, serializeTransaction, } from "viem";
17
- const DEFAULT_SUI_RPC = "https://fullnode.testnet.sui.io:443";
17
+ const FALLBACK_SUI_RPC = "https://fullnode.testnet.sui.io:443";
18
+ async function testRpcEndpoint(url) {
19
+ try {
20
+ const res = await fetch(url, {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/json" },
23
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "suix_getLatestSuiSystemState", params: [] }),
24
+ });
25
+ return res.ok;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function isRateLimitError(err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ return message.includes("429") || message.includes("Too Many Requests");
34
+ }
35
+ async function withRetry(fn, opts) {
36
+ const maxRetries = opts?.maxRetries ?? 3;
37
+ const baseDelayMs = opts?.baseDelayMs ?? 2000;
38
+ const label = opts?.label ?? "operation";
39
+ for (let attempt = 0;; attempt++) {
40
+ try {
41
+ return await fn();
42
+ }
43
+ catch (err) {
44
+ const lastAttempt = attempt >= maxRetries;
45
+ if (lastAttempt || !isRateLimitError(err)) {
46
+ throw err;
47
+ }
48
+ const delayMs = baseDelayMs * 2 ** attempt;
49
+ console.warn(`[KairoSDK] ${label} rate-limited (attempt ${attempt + 1}/${maxRetries + 1}); retrying in ${delayMs / 1000}s...`);
50
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
51
+ }
52
+ }
53
+ }
18
54
  const DEFAULT_EVM_RPC_URLS = {
19
55
  1: "https://rpc.ankr.com/eth",
20
56
  11155111: "https://rpc.ankr.com/eth_sepolia",
@@ -37,9 +73,30 @@ export class KairoClient {
37
73
  constructor(opts) {
38
74
  this.backend = new BackendClient({ backendUrl: opts.backendUrl, apiKey: opts.apiKey });
39
75
  this.store = new KeyStore(opts.storePath);
40
- this.suiRpcUrl = opts.suiRpcUrl ?? DEFAULT_SUI_RPC;
41
76
  this.network = opts.network ?? "testnet";
42
77
  this.evmRpcUrls = { ...DEFAULT_EVM_RPC_URLS, ...(opts.evmRpcUrls ?? {}) };
78
+ // Sui RPC priority: explicit option > env var > will resolve lazily (proxy or fallback)
79
+ this.suiRpcUrl =
80
+ opts.suiRpcUrl ??
81
+ process.env.SUI_RPC_URL?.trim() ??
82
+ ""; // Empty means we'll resolve lazily
83
+ }
84
+ /**
85
+ * Resolve Sui RPC URL, testing backend proxy first and falling back to public RPC.
86
+ */
87
+ async resolveSuiRpcUrl() {
88
+ if (this.suiRpcUrl)
89
+ return this.suiRpcUrl;
90
+ // Try backend proxy first (uses Shinami)
91
+ const backendProxy = `${this.backend.getBaseUrl()}/api/sui-rpc`;
92
+ if (await testRpcEndpoint(backendProxy)) {
93
+ this.suiRpcUrl = backendProxy;
94
+ return this.suiRpcUrl;
95
+ }
96
+ // Fall back to public RPC
97
+ console.warn("[KairoSDK] Backend RPC proxy not available, falling back to public Sui RPC");
98
+ this.suiRpcUrl = FALLBACK_SUI_RPC;
99
+ return this.suiRpcUrl;
43
100
  }
44
101
  /**
45
102
  * Create a new dWallet. Runs DKG on the agent's machine, submits to backend,
@@ -53,7 +110,8 @@ export class KairoClient {
53
110
  const seed = generateSeed();
54
111
  const encryptionKeys = await deriveEncryptionKeys(seed, curve);
55
112
  // 2. Fetch protocol params from Ika network (runs locally, avoids backend memory limit)
56
- const protocolParams = await fetchProtocolParams(curve, this.suiRpcUrl, this.network);
113
+ const rpcUrl = await this.resolveSuiRpcUrl();
114
+ const protocolParams = await withRetry(() => fetchProtocolParams(curve, rpcUrl, this.network), { label: "fetchProtocolParams" });
57
115
  // 3. Generate session identifier
58
116
  const sessionIdentifier = generateSessionIdentifier();
59
117
  // 4. Get backend admin address (DKG must target the admin signer)
@@ -323,7 +381,8 @@ export class KairoClient {
323
381
  presignId = presign.presignId;
324
382
  presignBytes = presign.presignBytes;
325
383
  }
326
- const protocolParams = await fetchProtocolParams("secp256k1", this.suiRpcUrl, this.network);
384
+ const rpcUrlForSign = await this.resolveSuiRpcUrl();
385
+ const protocolParams = await withRetry(() => fetchProtocolParams("secp256k1", rpcUrlForSign, this.network), { label: "fetchProtocolParams(sign)" });
327
386
  const messageBytes = new Uint8Array(Buffer.from(messageHexNoPrefix, "hex"));
328
387
  const userSignMessage = await this.computeUserSignMessageWithExtensionFallback(wallet, protocolParams, presignBytes, messageBytes);
329
388
  const policyContext = opts?.policyContext ?? {
@@ -474,7 +533,33 @@ export class KairoClient {
474
533
  async activateWallet(walletId, encryptedShareId, encryptionKeys, userPublicOutput) {
475
534
  // Wait a moment for the dWallet state to propagate
476
535
  await new Promise((r) => setTimeout(r, 2000));
477
- const dWallet = await fetchDWallet(this.suiRpcUrl, this.network, walletId);
536
+ const rpcUrlForActivation = await this.resolveSuiRpcUrl();
537
+ const dWallet = await withRetry(() => fetchDWallet(rpcUrlForActivation, this.network, walletId), { label: "fetchDWallet(activate)" });
538
+ // Check dWallet state - skip activation if already active
539
+ const state = dWallet?.state;
540
+ const isActive = Boolean(state?.Active) || state?.$kind === "Active";
541
+ if (isActive) {
542
+ // Already activated, nothing to do
543
+ return;
544
+ }
545
+ const isAwaitingSignature = Boolean(state?.AwaitingKeyHolderSignature) ||
546
+ state?.$kind === "AwaitingKeyHolderSignature";
547
+ if (!isAwaitingSignature) {
548
+ // Unknown state - wait and retry once
549
+ await new Promise((r) => setTimeout(r, 3000));
550
+ const dWallet2 = await withRetry(() => fetchDWallet(rpcUrlForActivation, this.network, walletId), { label: "fetchDWallet(activate-recheck)" });
551
+ const state2 = dWallet2?.state;
552
+ const isActive2 = Boolean(state2?.Active) || state2?.$kind === "Active";
553
+ if (isActive2)
554
+ return;
555
+ const isAwaiting2 = Boolean(state2?.AwaitingKeyHolderSignature) ||
556
+ state2?.$kind === "AwaitingKeyHolderSignature";
557
+ if (!isAwaiting2) {
558
+ const stateKind = state2?.$kind ?? Object.keys(state2 ?? {})[0] ?? "Unknown";
559
+ throw new Error(`dWallet is not ready for activation (state=${stateKind}). ` +
560
+ `This can happen if the DKG is still processing. Wait a few seconds and retry.`);
561
+ }
562
+ }
478
563
  const signature = await computeUserOutputSignature({
479
564
  encryptionKeys,
480
565
  dWallet,
@@ -12,22 +12,43 @@ function resolveCurve(curve) {
12
12
  }
13
13
  // Protocol params are large (~MB). Cache per curve to avoid refetching.
14
14
  const paramsCache = new Map();
15
+ // Reuse a single initialized Ika client per (network + RPC URL) to avoid
16
+ // repeated initialize() bursts that can trigger upstream rate limits.
17
+ const clientCache = new Map();
18
+ function getClientCacheKey(network, suiRpcUrl) {
19
+ return `${network}:${suiRpcUrl}`;
20
+ }
21
+ function getOrCreateIkaClient(network, suiRpcUrl) {
22
+ const key = getClientCacheKey(network, suiRpcUrl);
23
+ const cached = clientCache.get(key);
24
+ if (cached)
25
+ return cached;
26
+ const suiClient = new SuiClient({ url: suiRpcUrl });
27
+ const ikaConfig = getNetworkConfig(network);
28
+ const ikaClient = new IkaClient({ suiClient, config: ikaConfig });
29
+ const ready = ikaClient.initialize().catch((err) => {
30
+ clientCache.delete(key);
31
+ throw err;
32
+ });
33
+ const created = { ikaClient, ready };
34
+ clientCache.set(key, created);
35
+ return created;
36
+ }
15
37
  /**
16
38
  * Fetch Ika protocol public parameters for a given curve.
17
39
  * Uses the IkaClient which reads from the Ika coordinator on Sui.
18
40
  */
19
41
  export async function fetchProtocolParams(curve, suiRpcUrl, network = "testnet") {
20
42
  const ikaCurve = resolveCurve(curve);
21
- const cached = paramsCache.get(ikaCurve);
43
+ const paramsCacheKey = `${getClientCacheKey(network, suiRpcUrl)}:${ikaCurve}`;
44
+ const cached = paramsCache.get(paramsCacheKey);
22
45
  if (cached)
23
46
  return cached;
24
- const suiClient = new SuiClient({ url: suiRpcUrl });
25
- const ikaConfig = getNetworkConfig(network);
26
- const ikaClient = new IkaClient({ suiClient, config: ikaConfig });
27
- await ikaClient.initialize();
47
+ const { ikaClient, ready } = getOrCreateIkaClient(network, suiRpcUrl);
48
+ await ready;
28
49
  // First arg is dWallet (undefined for new wallets), second is curve
29
50
  const params = await ikaClient.getProtocolPublicParameters(undefined, ikaCurve);
30
- paramsCache.set(ikaCurve, params);
51
+ paramsCache.set(paramsCacheKey, params);
31
52
  return params;
32
53
  }
33
54
  /**
@@ -73,10 +94,8 @@ export async function computeUserOutputSignature(params) {
73
94
  * Fetch the dWallet object from Ika network for activation.
74
95
  */
75
96
  export async function fetchDWallet(suiRpcUrl, network, dwalletId) {
76
- const suiClient = new SuiClient({ url: suiRpcUrl });
77
- const ikaConfig = getNetworkConfig(network);
78
- const ikaClient = new IkaClient({ suiClient, config: ikaConfig });
79
- await ikaClient.initialize();
97
+ const { ikaClient, ready } = getOrCreateIkaClient(network, suiRpcUrl);
98
+ await ready;
80
99
  return ikaClient.getDWallet(dwalletId);
81
100
  }
82
101
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kairoguard/sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",