Skill: okwme/trifle-auth (version 1.1.0)

--- name: trifle-auth description: Authenticate with the Trifle API using Sign-In with Ethereum (SIWE). Manages wallet-based authentication, JWT token storage, and session management for the Trifle ecosystem. version: 1.0.0 metadata: clawdhub: emoji: "🔐" requires: bins: ["node"] npm: ["viem", "siwe"] platforms: ["api"] --- # Trifle Auth Skill Authenticate with the Trifle API (`bot.trifle.life`) using SIWE (Sign-In with Ethereum) for wallet-based authentication. Stores JWT tokens for use by other skills (snake-game, etc.). ## Setup ### 1. Install dependencies ```bash cd ~/.openclaw/workspace/skills/trifle-auth npm install ``` ### 2. Generate a wallet ```bash node trifle-auth.mjs generate ``` This generates a new Ethereum address and **automatically saves the private key to 1Password** (vault: Gigi, item: "EVM Wallet - Trifle Agent"). The key is never printed to stdout. If 1Password is unavailable, it falls back to a restricted file (`~/.trifle-wallet.key`, mode 600). ### 3. Set environment (or use 1Password) The skill reads the private key from: 1. `TRIFLE_PRIVATE_KEY` environment variable (first priority) 2. 1Password: `op://Gigi/EVM Wallet - Gigi/private_key` <!-- nocheck --> ### 4. Login ```bash node trifle-auth.mjs login ``` This authenticates via SIWE and stores the JWT token in `~/.local/state/trifle-auth/auth-state.json`. ## Commands ```bash # Authenticate and store JWT token node trifle-auth.mjs login # Check auth status and user info node trifle-auth.mjs status # Get JWT token path (written to a secure temp file, not stdout) TOKEN=$(cat $(node trifle-auth.mjs token)) node trifle-auth.mjs token # Generate a new wallet keypair node trifle-auth.mjs generate # Check ball balance node trifle-auth.mjs balance ``` ## Authentication Flow 1. Gets nonce from `/auth/wallet/nonce` 2. Creates SIWE message with domain `trifle.life` 3. Signs message with private key (no gas cost) 4. Verifies signature at `/auth/wallet/verify` 5. Receives JWT token (valid 30 days) 6. Stores token in state file ## State File Location: `~/.local/state/trifle-auth/auth-state.json` (XDG-compliant, override with `TRIFLE_AUTH_STATE` env var) ```json { "token": "jwt-token-here", "address": "0x...", "userId": 123, "username": "gigi", "totalBalls": 50, "lastLogin": "2026-02-05T15:00:00Z" } ``` ## CORS Notes API requests include `Origin: https://trifle.life` header to satisfy CORS requirements. The backend validates the origin against its allowed list. ## Ball Economy - **Earning balls**: GM game (1/day), auth bonuses (10 per platform), reactions, etc. - **Spending balls**: Snake game votes (min 1 per vote) - **Auth bonuses**: Each platform auth earns 10 balls (wallet, discord, telegram, etc.)

path: clawdhub.json
content: {
  "name": "trifle-auth",
  "version": "1.1.0",
  "description": "Authenticate with the Trifle API using Sign-In with Ethereum (SIWE). Manages wallet-based authentication, JWT token storage, and session management for the Trifle ecosystem.",
  "author": "trifle-labs",
  "license": "MIT",
  "repository": "https://github.com/trifle-labs/skills",
  "homepage": "https://github.com/trifle-labs/skills/tree/main/trifle-auth",
  "emoji": "🔐",
  "requires": {
    "bins": ["node"]
  },
  "files": [
    "trifle-auth.mjs",
    "SKILL.md",
    "package.json",
    "package-lock.json"
  ],
  "install": {
    "post": "chmod +x trifle-auth.mjs && npm install"
  }
}


path: package-lock.json
content: {
  "name": "trifle-auth",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "trifle-auth",
      "version": "1.0.0",
      "license": "ISC",
      "dependencies": {
        "siwe": "^3.0.0",
        "viem": "^2.45.1"
      }
    },
    "node_modules/@adraffy/ens-normalize": {
      "version": "1.10.1",
      "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
      "integrity": [REDACTED_SECRET],
      "peer": true
    },
    "node_modules/@noble/ciphers": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@noble/curves": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "peer": true,
      "dependencies": {
        "@noble/hashes": "1.3.2"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@noble/hashes": {
      "version": "1.3.2",
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@scure/base": {
      "version": "1.2.6",
      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@scure/bip32": {
      "version": "1.7.0",
      "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@noble/curves": "~1.9.0",
        "@noble/hashes": "~1.8.0",
        "@scure/base": "~1.2.5"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@scure/bip32/node_modules/@noble/curves": {
      "version": "1.9.7",
      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@noble/hashes": "1.8.0"
      },
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@scure/bip32/node_modules/@noble/hashes": {
      "version": "1.8.0",
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@scure/bip39": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@noble/hashes": "~1.8.0",
        "@scure/base": "~1.2.5"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@scure/bip39/node_modules/@noble/hashes": {
      "version": "1.8.0",
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@spruceid/siwe-parser": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/@spruceid/siwe-parser/-/siwe-parser-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@noble/hashes": "^1.1.2",
        "apg-js": "^4.4.0"
      }
    },
    "node_modules/@stablelib/binary": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@stablelib/int": "^1.0.1"
      }
    },
    "node_modules/@stablelib/int": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/@stablelib/random": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@stablelib/binary": "^1.0.1",
        "@stablelib/wipe": "^1.0.1"
      }
    },
    "node_modules/@stablelib/wipe": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/@types/node": {
      "version": "22.7.5",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
      "integrity": [REDACTED_SECRET],
      "peer": true,
      "dependencies": {
        "undici-types": "~6.19.2"
      }
    },
    "node_modules/abitype": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": {
        "url": "https://github.com/sponsors/wevm"
      },
      "peerDependencies": {
        "typescript": ">=5.0.4",
        "zod": "^3.22.0 || ^4.0.0"
      },
      "peerDependenciesMeta": {
        "typescript": {
          "optional": true
        },
        "zod": {
          "optional": true
        }
      }
    },
    "node_modules/aes-js": {
      "version": "4.0.0-beta.5",
      "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
      "integrity": [REDACTED_SECRET],
      "peer": true
    },
    "node_modules/apg-js": {
      "version": "4.4.0",
      "resolved": "https://registry.npmjs.org/apg-js/-/apg-js-4.4.0.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/ethers": {
      "version": "6.16.0",
      "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/ethers-io/"
        },
        {
          "type": "individual",
          "url": "https://www.buymeacoffee.com/ricmoo"
        }
      ],
      "peer": true,
      "dependencies": {
        "@adraffy/ens-normalize": "1.10.1",
        "@noble/curves": "1.2.0",
        "@noble/hashes": "1.3.2",
        "@types/node": "22.7.5",
        "aes-js": "4.0.0-beta.5",
        "tslib": "2.7.0",
        "ws": "8.17.1"
      },
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/eventemitter3": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/isows": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/wevm"
        }
      ],
      "peerDependencies": {
        "ws": "*"
      }
    },
    "node_modules/ox": {
      "version": "0.11.3",
      "resolved": "https://registry.npmjs.org/ox/-/ox-0.11.3.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/wevm"
        }
      ],
      "dependencies": {
        "@adraffy/ens-normalize": "^1.11.0",
        "@noble/ciphers": "^1.3.0",
        "@noble/curves": "1.9.1",
        "@noble/hashes": "^1.8.0",
        "@scure/bip32": "^1.7.0",
        "@scure/bip39": "^1.6.0",
        "abitype": "^1.2.3",
        "eventemitter3": "5.0.1"
      },
      "peerDependencies": {
        "typescript": ">=5.4.0"
      },
      "peerDependenciesMeta": {
        "typescript": {
          "optional": true
        }
      }
    },
    "node_modules/ox/node_modules/@adraffy/ens-normalize": {
      "version": "1.11.1",
      "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/ox/node_modules/@noble/curves": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@noble/hashes": "1.8.0"
      },
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/ox/node_modules/@noble/hashes": {
      "version": "1.8.0",
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/siwe": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/siwe/-/siwe-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@spruceid/siwe-parser": "^3.0.0",
        "@stablelib/random": "^1.0.1"
      },
      "peerDependencies": {
        "ethers": "^5.6.8 || ^6.0.8"
      }
    },
    "node_modules/tslib": {
      "version": "2.7.0",
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "peer": true
    },
    "node_modules/undici-types": {
      "version": "6.19.8",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
      "integrity": [REDACTED_SECRET],
      "peer": true
    },
    "node_modules/viem": {
      "version": "2.45.1",
      "resolved": "https://registry.npmjs.org/viem/-/viem-2.45.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/wevm"
        }
      ],
      "dependencies": {
        "@noble/curves": "1.9.1",
        "@noble/hashes": "1.8.0",
        "@scure/bip32": "1.7.0",
        "@scure/bip39": "1.6.0",
        "abitype": "1.2.3",
        "isows": "1.0.7",
        "ox": "0.11.3",
        "ws": "8.18.3"
      },
      "peerDependencies": {
        "typescript": ">=5.0.4"
      },
      "peerDependenciesMeta": {
        "typescript": {
          "optional": true
        }
      }
    },
    "node_modules/viem/node_modules/@noble/curves": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@noble/hashes": "1.8.0"
      },
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/viem/node_modules/@noble/hashes": {
      "version": "1.8.0",
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": "^14.21.3 || >=16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/viem/node_modules/ws": {
      "version": "8.18.3",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    },
    "node_modules/ws": {
      "version": "8.17.1",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
      "integrity": [REDACTED_SECRET],
      "peer": true,
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    }
  }
}


path: package.json
content: {
  "name": "trifle-auth",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "siwe": "^3.0.0",
    "viem": "^2.45.1"
  }
}


path: trifle-auth.mjs
content: #!/usr/bin/env node
/**
 * trifle-auth: Authenticate with trifle-bot API using SIWE (Sign-In with Ethereum)
 *
 * Commands:
 *   login       - Authenticate and get JWT token (stored in state file)
 *   status      - Check current auth status and user info
 *   token       - Print current JWT token (for use by other skills)
 *   generate    - Generate a new wallet (prints to stdout, user stores in 1Password)
 *   balance     - Check ball balance
 *
 * Environment:
 *   TRIFLE_PRIVATE_KEY - Ethereum private key (or read from 1Password)
 *   TRIFLE_BACKEND_URL - Backend URL (default: https://bot.trifle.life)
 */

import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
import { createSiweMessage } from 'viem/siwe';
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
import { dirname, join } from 'path';
import { spawnSync } from 'child_process';
import { randomBytes } from 'crypto';
import { tmpdir } from 'os';

// Server options (shared with snake-game)
const SERVERS = {
  live: 'https://bot.trifle.life',
  staging: 'https://bot-staging.trifle.life',
};

// XDG-compliant paths — isolated from host agent internals
const HOME = process.env.HOME;
const XDG_CONFIG = process.env.XDG_CONFIG_HOME || join(HOME, '.config');
const XDG_STATE  = process.env.XDG_STATE_HOME  || join(HOME, '.local/state');
const CONFIG_DIR  = join(XDG_CONFIG, 'trifle-auth');
const STATE_DIR   = join(XDG_STATE,  'trifle-auth');

const STATE_FILE = process.env.TRIFLE_AUTH_STATE || join(STATE_DIR, 'auth-state.json');
const SETTINGS_FILE = join(CONFIG_DIR, 'settings.json');
const BACKEND_URL = process.env.TRIFLE_BACKEND_URL || SERVERS.live;

function loadServerSettings() {
  try {
    if (existsSync(SETTINGS_FILE)) {
      return JSON.parse(readFileSync(SETTINGS_FILE, 'utf8'));
    }
  } catch {}
  return { server: 'live' };
}

// Ensure state and config directories exist
mkdirSync(STATE_DIR, { recursive: true });
mkdirSync(CONFIG_DIR, { recursive: true });

function loadState() {
  try {
    return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
  } catch {
    return { token: null, address: null, userId: null, username: null, lastLogin: null };
  }
}

function saveState(state) {
  writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}

function getPrivateKey() {
  // Try environment variable first
  if (process.env.TRIFLE_PRIVATE_KEY) {
    return process.env.TRIFLE_PRIVATE_KEY;
  }

  // Try 1Password via spawnSync (no shell — avoids injection risk, never logs key)
  try {
    const result = spawnSync(
      'op', ['read', 'op://Gigi/EVM Wallet - Gigi/private_key'], // nocheck
      { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
    );
    const key = result.stdout?.trim();
    if (key && !result.error) return key;
  } catch {
    // 1Password not available or item not found
  }

  console.error('Error: No private key found.');
  console.error('Set TRIFLE_PRIVATE_KEY env var or store key in 1Password as "EVM Wallet - Gigi"'); // nocheck
  process.exit(1);
}

async function apiRequest(path, options = {}) {
  const url = `${BACKEND_URL}${path}`;
  const res = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'Origin': 'https://trifle.life',
      'Referer': 'https://trifle.life/',
      ...options.headers,
    },
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`API error ${res.status}: ${text}`);
  }

  return res.json();
}

async function authenticatedRequest(path, options = {}) {
  const state = loadState();
  if (!state.token) {
    throw new Error('Not authenticated. Run: trifle-auth.mjs login');
  }

  return apiRequest(path, {
    ...options,
    headers: {
      'Authorization': `Bearer ${state.token}`,
      ...options.headers,
    },
  });
}

// === Commands ===

async function cmdLogin() {
  const privateKey = getPrivateKey();
  const account = privateKeyToAccount(privateKey);

  console.log(`Authenticating wallet: ${account.address}`);

  // 1. Get nonce
  const { nonce } = await apiRequest('/auth/wallet/nonce', {
    method: 'POST',
  });
  console.log(`Got nonce: ${nonce}`);

  // 2. Create SIWE message
  const message = createSiweMessage({
    domain: 'trifle.life',
    address: account.address,
    statement: 'Sign this message to prove you own this wallet (at no cost to you).',
    uri: 'https://trifle.life',
    version: '1',
    chainId: mainnet.id,
    nonce,
  });

  // 3. Sign message
  const walletClient = createWalletClient({
    account,
    chain: mainnet,
    transport: http(),
  });

  const signature = await walletClient.signMessage({ message });
  console.log('Message signed');

  // 4. Verify and get token
  const result = await apiRequest('/auth/wallet/verify', {
    method: 'POST',
    body: JSON.stringify({
      signature,
      message,
      chainId: mainnet.id,
    }),
  });

  if (!result.token) {
    console.error('Authentication failed:', JSON.stringify(result));
    process.exit(1);
  }

  // 5. Get user info
  let userInfo = {};
  try {
    const statusRes = await apiRequest('/auth/status', {
      headers: { 'Authorization': `Bearer ${result.token}` },
    });
    userInfo = statusRes.user || statusRes;
  } catch (e) {
    console.warn('Could not fetch user info:', e.message);
  }

  // 6. Save state
  const state = {
    [REDACTED_SECRET],
    address: account.address,
    userId: userInfo.id || null,
    username: userInfo.username || null,
    totalBalls: userInfo.totalBalls || null,
    lastLogin: new Date().toISOString(),
  };
  saveState(state);

  console.log('=== Authentication Successful ===');
  console.log(`Address: ${account.address}`);
  console.log(`User ID: ${state.userId}`);
  console.log(`Username: ${state.username}`);
  console.log(`Balls: ${state.totalBalls}`);
  console.log(`Token saved to: ${STATE_FILE}`);
}

async function cmdStatus() {
  const state = loadState();

  if (!state.token) {
    console.log('Status: NOT AUTHENTICATED');
    console.log('Run: trifle-auth.mjs login');
    return;
  }

  try {
    const result = await authenticatedRequest('/auth/status');
    const user = result.user || result;

    // Update stored state
    state.userId = user.id;
    state.username = user.username;
    state.totalBalls = user.totalBalls;
    saveState(state);

    console.log('=== Trifle Auth Status ===');
    console.log(`Address: ${state.address}`);
    console.log(`User ID: ${user.id}`);
    console.log(`Username: ${user.username}`);
    console.log(`Balls: ${user.totalBalls}`);
    console.log(`Last Login: ${state.lastLogin}`);
    console.log(`Platforms: ${(user.platforms || []).map(p => p.type).join(', ') || 'none'}`);
  } catch (e) {
    console.log('Status: TOKEN EXPIRED or INVALID');
    console.log(`Error: ${e.message}`);
    console.log('Run: trifle-auth.mjs login');
  }
}

async function cmdToken() {
  const state = loadState();
  if (!state.token) {
    console.error('Not authenticated. Run: trifle-auth.mjs login');
    process.exit(1);
  }
  // Write token to a restricted temp file rather than stdout to avoid log exposure.
  // Callers read the file: TOKEN=$(cat $(node trifle-auth.mjs token))
  const tmpFile = join(tmpdir(), `trifle-token-${process.pid}.tmp`);
  writeFileSync(tmpFile, state.token, { mode: 0o600 });
  // Clean up on exit
  process.on('exit', () => { try { unlinkSync(tmpFile); } catch {} });
  // Print only the file path to stdout
  process.stdout.write(tmpFile);
}

async function cmdGenerate() {
  const privateKey = '0x' + randomBytes(32).toString('hex');
  const account = privateKeyToAccount(privateKey);

  console.log('=== New Wallet Generated ===');
  console.log(`Address: ${account.address}`);
  console.log('');

  // Attempt to save directly to 1Password (never prints key to stdout/logs)
  const opResult = spawnSync(
    'op', [
      'item', 'create',
      '--category', 'Login',
      '--title', 'EVM Wallet - Trifle Agent',
      '--vault', 'Gigi',
      `private_key=${privateKey}`, // nocheck — passed directly to op CLI, never logged
      `address=${account.address}`,
    ],
    { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
  );

  if (!opResult.error && opResult.status === 0) {
    console.log('✅ Private key saved directly to 1Password (vault: Gigi)');
    console.log('   Item: "EVM Wallet - Trifle Agent"');
    console.log('   Set TRIFLE_KEY env var or update trifle-auth.mjs op:// path to use it.');
  } else {
    // 1Password unavailable — write to a restricted file, never stdout
    const keyFile = join(process.env.HOME, '.trifle-wallet.key');
    writeFileSync(keyFile, privateKey, { mode: 0o600 }); // nocheck — writing to restricted file, not stdout
    console.log(`⚠️  1Password unavailable. Private key written to: ${keyFile} (mode 600)`);
    console.log('   Move it to a secure vault as soon as possible.');
    console.log('   NEVER commit or share this file.');
  }

  console.log('');
  console.log('Next: run "node trifle-auth.mjs login" to authenticate.');
}

async function cmdBalance() {
  const state = loadState();

  if (!state.token) {
    console.error('Not authenticated. Run: trifle-auth.mjs login');
    process.exit(1);
  }

  try {
    const result = await authenticatedRequest('/balls');
    console.log(`Balance: ${result.totalBalls || result.balance || JSON.stringify(result)} balls`);
  } catch (e) {
    // Try the public endpoint with username
    if (state.username) {
      try {
        const result = await apiRequest(`/balls/user?username=${state.username}`);
        console.log(`Balance: ${result.totalBalls || JSON.stringify(result)} balls`);
        return;
      } catch {}
    }
    console.error(`Error: ${e.message}`);
  }
}

// === Main ===

const command = process.argv[2];

switch (command) {
  case 'login':
    await cmdLogin();
    break;
  case 'status':
    await cmdStatus();
    break;
  case 'token':
    await cmdToken();
    break;
  case 'generate':
    await cmdGenerate();
    break;
  case 'balance':
    await cmdBalance();
    break;
  default:
    console.log('trifle-auth: Authenticate with Trifle API via SIWE');
    console.log('');
    console.log('Commands:');
    console.log('  login      Authenticate with wallet and get JWT token');
    console.log('  status     Check current auth status and user info');
    console.log('  token      Print current JWT token (for piping)');
    console.log('  generate   Generate a new wallet keypair');
    console.log('  balance    Check ball balance');
    console.log('');
    console.log('Setup:');
    console.log('  1. Generate wallet: node trifle-auth.mjs generate');
    console.log('  2. Store private key in 1Password as "Trifle Bot Wallet"');
    console.log('  3. Login: node trifle-auth.mjs login');
    console.log('');
    console.log('Environment:');
    console.log('  TRIFLE_PRIVATE_KEY    - Private key (or use 1Password)');
    console.log('  TRIFLE_BACKEND_URL    - API URL (default: https://bot.trifle.life)');
    break;
}


Security scan verdicts — ClawScan: suspicious | static analysis:  | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)