Skill: zbruceli/dchat (version 0.4.0)

--- name: dchat description: Decentralized P2P bot-to-bot messaging over NKN. Send and receive text, images, audio, and files without any centralized server. Private, encrypted, serverless. user-invocable: true metadata: openclaw: emoji: "\U0001F4AC" requires: bins: - node --- # dchat — Decentralized P2P Messaging Skill You have access to **dchat**, a decentralized peer-to-peer messaging system over the NKN relay network. Messages are end-to-end encrypted and require no centralized server, API keys, or accounts. Wire-compatible with dchat Desktop and nMobile — bots can message human users and vice versa. ## Commands Run these commands using your shell execution tool. The `dchat` wrapper is in the skill directory at `{baseDir}`. ### Identity (no network needed) ```bash {baseDir}/dchat init # Generate bot identity (first time, auto-run at install) {baseDir}/dchat address # Print bot's NKN address ``` ### Contacts (no network needed) ```bash {baseDir}/dchat contacts add <address> <alias> # Add a contact with alias {baseDir}/dchat contacts remove <alias> # Remove a contact {baseDir}/dchat contacts list # List all contacts ``` Once a contact is added, use the alias instead of the 64-char address in send, history, and other commands. ### Messaging (connects to NKN, ~5-8s per send) ```bash {baseDir}/dchat send <address> Hello world! # Send text message {baseDir}/dchat send-image <address> ./photo.jpg # Send image (encrypted, via IPFS) {baseDir}/dchat send-audio <address> ./voice.aac # Send audio file {baseDir}/dchat send-file <address> ./report.pdf # Send any file (encrypted, via IPFS) ``` Output on success: `Connecting...` then `Sent: <message-id>`. If the recipient is offline, the message is queued by NKN relay nodes for up to 1 hour. ### Receiving ```bash {baseDir}/dchat listen # Listen for messages (daemon, Ctrl+C to stop) ``` Output format: ``` [HH:MM:SS] <sender-prefix>...: <text> [HH:MM:SS] <sender-prefix>...: [image] /path/to/downloaded.jpg [HH:MM:SS] <sender-prefix>...: [audio] /path/to/downloaded.aac [HH:MM:SS] <sender-prefix>...: [file: report.pdf] /path/to/downloaded.pdf ``` ### History (no network needed) ```bash {baseDir}/dchat history <address> # Show last 50 messages with peer {baseDir}/dchat history <address> 100 # Show last 100 messages ``` ### Interactive REPL ```bash {baseDir}/dchat interactive # Full interactive mode with /send /file etc. ``` ### Options ```bash {baseDir}/dchat --seed <64-char-hex> address # Use specific seed {baseDir}/dchat --data-dir /custom/path init # Custom data directory ``` ## When to Use Use this skill when the user or another agent wants to: - Send or receive messages to/from another bot or NKN user - Transfer files, images, or audio to a peer - Set up a listening bot that reacts to incoming messages - Check message history with a peer - Get the bot's NKN address to share with others ## Key Concepts - **NKN Address**: 64-char hex public key. Each bot gets a unique address derived from its seed. Can be derived offline. - **Seed**: 64-char hex Ed25519 private key. Auto-generated at install, saved encrypted to `~/.dchat-clawhub/identity.enc`. - **Media**: Encrypted with AES-128-GCM, uploaded to IPFS, key sent over NKN. Recipients auto-download and decrypt. - **History**: All messages stored in SQLite at `~/.dchat-clawhub/messages.db`. - **Receipts**: Delivery and read receipts are automatic. ## Security - AES-128-GCM encryption for all media before IPFS upload - Identity encrypted at rest (AES-256-GCM + PBKDF2, 100K iterations). Encryption key (.passkey) stored in separate OS config directory (~/.config/dchat-clawhub/) from the ciphertext (~/.dchat-clawhub/), both with file permissions 0600 - No centralized server — messages travel through NKN relay nodes - SSRF protection: ad-hoc IPFS gateways from incoming messages are DNS-resolved and all IPs verified as public (rejects RFC 1918, loopback, link-local, ULA, IPv4-mapped IPv6) ## Network & Storage - Connects to NKN seed RPC servers (seed.nkn.org) for P2P relay - Uploads/downloads encrypted media via public IPFS gateways (ipfs.io, dweb.link) - Stores messages in local SQLite database (~/.dchat-clawhub/messages.db) - Caches downloaded media in ~/.dchat-clawhub/media-cache/ - Install pulls 3 production npm packages: nkn-sdk, better-sqlite3, form-data ## Limitations - Each send takes ~5-8 seconds (NKN connect + 3s stabilization + relay dispatch) - Offline messages held by NKN relay nodes for up to 1 hour - Max file size: 100 MB - Requires internet access to reach NKN seed nodes

path: CLAUDE.md
content: # CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What This Is

dchat-clawhub is a ClawHub skill for headless bot-to-bot P2P communication over the NKN relay network. Wire-compatible with dchat Desktop and nMobile.

## Commands

```bash
npm run build          # Compile TypeScript → dist/
npm run test           # Unit tests (vitest, 20 tests)
npm run test:e2e       # E2E self-echo over live NKN network
npm run typecheck      # Type check without emitting
```

Run a single test file: `npx vitest run tests/crypto.test.ts`

## Publishing

```bash
npm run build                    # Must build before publish (dist/ ships pre-built)
clawhub publish . --slug dchat --name dchat --version X.Y.Z --changelog "..."
```

Install script uses `npm install --omit=dev` — only 3 production deps are needed at runtime.

## Architecture

```
SKILL.md      — ClawHub skill manifest (frontmatter + agent instructions)
dchat         — Wrapper script (entry point for skill invocation)
install.sh    — First-time setup: npm install --omit=dev + init identity
src/
  index.ts    — Library exports (public API)
  bot.ts      — DchatBot: orchestrator, event emitter, send/receive
  client.ts   — NknClient: NKN MultiClient wrapper (4 sub-clients)
  crypto.ts   — AES-128-GCM encrypt/decrypt (nMobile-compatible)
  ipfs.ts     — IpfsService: IPFS upload/download with DNS-based SSRF protection
  media.ts    — MediaService: image/audio/file encrypt + IPFS transfer
  storage.ts  — SafeStorage: encrypted identity (AES-256-GCM + PBKDF2)
  db.ts       — MessageDb: SQLite message store + peer tracking
  types.ts    — TypeScript types (MessageData, MessageOptions, etc.)
  cli.ts      — CLI with sub-commands (send, listen, history, etc.)
```

### Key Design Decisions

- **3-second stabilization delay** after NKN connect before sending. `onConnect` fires when the first sub-client connects, but sending needs all sub-clients' relay routes established.
- **Fire-and-forget for long-running bots** (`sendText`), **awaited send for CLI one-shot** (`sendTextAwait`). CLI uses `process.exit(0)` after confirmed dispatch to avoid NKN teardown noise.
- **Timeout = queued**: NKN "Message timeout" means relay accepted the message but recipient didn't ACK (offline). Message is still queued for up to 1 hour.
- **Passkey separated from ciphertext**: `.passkey` in `~/.config/dchat-clawhub/`, identity in `~/.dchat-clawhub/identity.enc`.
- **SSRF protection**: ad-hoc IPFS gateways from incoming messages are DNS-resolved; all IPs verified public before connecting.

### NKN Message Protocol

Messages are JSON `MessageData` objects sent as strings over NKN:
- `id` (UUID), `contentType`, `content`, `options` (encryption keys, file metadata), `timestamp`
- Media: encrypted with AES-128-GCM, uploaded to IPFS, key sent in `options.ipfsEncryptKeyBytes` as `number[]`
- Receipts: `contentType: "receipt"` with `targetID` pointing to original message

### Data Storage

- Encrypted identity: `~/.dchat-clawhub/identity.enc` (AES-256-GCM, mode 0600)
- Encryption passkey: `~/.config/dchat-clawhub/.passkey` (random, mode 0600)
- Messages: `~/.dchat-clawhub/messages.db` (SQLite, WAL mode)
- Media cache: `~/.dchat-clawhub/media-cache/`


path: README.md
content: # dchat-clawhub

A [ClawHub](https://clawhub.ai) skill for decentralized bot-to-bot P2P communication over [NKN](https://nkn.org) (New Kind of Network).

Gives AI agents the ability to send and receive encrypted messages, images, audio, and files — without any centralized server, API keys, or accounts. Wire-compatible with [dchat Desktop](https://github.com/nickytonline/dchat) and [nMobile](https://nmobile.nkn.org/).

## Install

```bash
clawhub install dchat
```

Or manually:

```bash
git clone https://github.com/zbruceli/dchat-clawhub.git ~/.openclaw/skills/dchat
cd ~/.openclaw/skills/dchat && ./install.sh
```

Installation auto-generates a bot identity (seed + NKN address) — no manual setup needed. The skill is immediately available as `/dchat` in any OpenClaw agent session.

## Commands

```bash
# Identity (no network needed)
dchat init                              # Generate bot identity
dchat address                           # Print NKN address

# Messaging (connects to NKN)
dchat send <address> Hello world!       # Send text
dchat send-image <address> ./photo.jpg  # Send image (encrypted via IPFS)
dchat send-audio <address> ./voice.aac  # Send audio
dchat send-file <address> ./report.pdf  # Send file (encrypted via IPFS)

# Receiving
dchat listen                            # Listen for messages (daemon)

# History (no network needed)
dchat history <address>                 # Last 50 messages with peer
dchat history <address> 100             # Last 100

# Interactive
dchat interactive                       # REPL with /send /file /history etc.
dchat help                              # Show all commands
```

## Why

- **No central server** — messages route through the NKN relay network, no API keys needed
- **Encrypted** — AES-128-GCM for all media, AES-256-GCM + PBKDF2 for identity at rest
- **Rich media** — text, images, audio, and files via IPFS
- **Lightweight** — pure Node.js, no Electron/GUI dependencies
- **Interoperable** — bots can message dchat Desktop and nMobile human users

## Skill Metadata

| Field | Value |
|-------|-------|
| Name | `dchat` |
| Slash command | `/dchat` |
| Dispatch | Model-invoked |
| Requirements | `node` on PATH |

## Architecture

```
SKILL.md        Skill manifest + agent instructions
dchat           Wrapper script (entry point)
install.sh      First-time setup (npm install --omit=dev + init)
src/
  bot.ts        DchatBot — orchestrator, event emitter
  client.ts     NKN MultiClient wrapper (4 sub-clients)
  crypto.ts     AES-128-GCM (nMobile-compatible wire format)
  ipfs.ts       IPFS upload/download with DNS-based SSRF protection
  media.ts      Image/audio/file encrypt + IPFS transfer
  storage.ts    Encrypted identity persistence (AES-256-GCM + PBKDF2)
  db.ts         SQLite message store + peer tracking
  types.ts      TypeScript type definitions
  cli.ts        CLI with sub-commands
  index.ts      Public API exports
```

## Development

```bash
npm install && npm run build   # setup
npm test                       # 20 unit tests
npm run test:e2e               # self-echo over live NKN network
npm run typecheck              # type check only
```

## License

MIT


path: install.sh
content: #!/bin/bash
# ClawHub skill installer for dchat-clawhub
set -e

SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SKILL_DIR"

# Only install production dependencies (nkn-sdk, better-sqlite3, form-data).
# TypeScript and dev tools are NOT needed — dist/ is pre-built and shipped.
echo "[dchat] Installing production dependencies..."
npm install --omit=dev

# Verify the pre-built CLI exists
if [ ! -f dist/cli.js ]; then
  echo "[dchat] ERROR: dist/cli.js not found. The skill package may be corrupt."
  exit 1
fi

echo "[dchat] Generating bot identity..."
node dist/cli.js init

echo "[dchat] Installed successfully."


path: package-lock.json
content: {
  "name": "dchat-clawhub",
  "version": "0.1.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "dchat-clawhub",
      "version": "0.1.0",
      "dependencies": {
        "better-sqlite3": "^11.7.0",
        "form-data": "^4.0.5",
        "nkn-sdk": "^1.3.6"
      },
      "bin": {
        "dchat-bot": "dist/cli.js"
      },
      "devDependencies": {
        "@types/better-sqlite3": "^7.6.12",
        "@types/node": "^22.0.0",
        "tsx": "^4.19.0",
        "typescript": "^5.7.0",
        "vitest": "^2.0.0"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@babel/polyfill": {
      "version": "7.12.1",
      "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.",
      "license": "MIT",
      "dependencies": {
        "core-js": "^2.6.5",
        "regenerator-runtime": "^0.13.4"
      }
    },
    "node_modules/@babel/polyfill/node_modules/core-js": {
      "version": "2.6.12",
      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
      "hasInstallScript": true,
      "license": "MIT"
    },
    "node_modules/@esbuild/aix-ppc64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "aix"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/android-arm": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/android-arm64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/android-x64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/darwin-arm64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/darwin-x64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/freebsd-arm64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/freebsd-x64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-arm": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-arm64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-ia32": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-loong64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-mips64el": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "mips64el"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-ppc64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-riscv64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-s390x": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/linux-x64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/netbsd-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-x64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/openbsd-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-x64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/openharmony-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/sunos-x64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "sunos"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/win32-arm64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/win32-ia32": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@esbuild/win32-x64": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@jridgewell/sourcemap-codec": {
      "version": "1.5.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@nkn/ncp": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/@nkn/ncp/-/ncp-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "core-js-pure": "^3.6.4",
        "google-protobuf": "^3.11.2",
        "heap": "^0.2.6",
        "setimmediate": "^1.0.5"
      }
    },
    "node_modules/@rollup/rollup-android-arm-eabi": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ]
    },
    "node_modules/@rollup/rollup-android-arm64": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ]
    },
    "node_modules/@rollup/rollup-darwin-arm64": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ]
    },
    "node_modules/@rollup/rollup-darwin-x64": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ]
    },
    "node_modules/@rollup/rollup-freebsd-arm64": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ]
    },
    "node_modules/@rollup/rollup-freebsd-x64": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm64-gnu": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm64-musl": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-loong64-gnu": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-loong64-musl": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-ppc64-musl": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-riscv64-musl": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-s390x-gnu": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-x64-gnu": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-x64-musl": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-openbsd-x64": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ]
    },
    "node_modules/@rollup/rollup-openharmony-arm64": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ]
    },
    "node_modules/@rollup/rollup-win32-arm64-msvc": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-ia32-msvc": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-x64-gnu": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-x64-msvc": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@types/better-sqlite3": {
      "version": "7.6.13",
      "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/node": "*"
      }
    },
    "node_modules/@types/estree": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/node": {
      "version": "22.19.15",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/@vitest/expect": {
      "version": "2.1.9",
      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/spy": "2.1.9",
        "@vitest/utils": "2.1.9",
        "chai": "^5.1.2",
        "tinyrainbow": "^1.2.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/mocker": {
      "version": "2.1.9",
      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/spy": "2.1.9",
        "estree-walker": "^3.0.3",
        "magic-string": "^0.30.12"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      },
      "peerDependencies": {
        "msw": "^2.4.9",
        "vite": "^5.0.0"
      },
      "peerDependenciesMeta": {
        "msw": {
          "optional": true
        },
        "vite": {
          "optional": true
        }
      }
    },
    "node_modules/@vitest/pretty-format": {
      "version": "2.1.9",
      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "tinyrainbow": "^1.2.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/runner": {
      "version": "2.1.9",
      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/utils": "2.1.9",
        "pathe": "^1.1.2"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/snapshot": {
      "version": "2.1.9",
      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/pretty-format": "2.1.9",
        "magic-string": "^0.30.12",
        "pathe": "^1.1.2"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/spy": {
      "version": "2.1.9",
      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "tinyspy": "^3.0.2"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/@vitest/utils": {
      "version": "2.1.9",
      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/pretty-format": "2.1.9",
        "loupe": "^3.1.2",
        "tinyrainbow": "^1.2.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/assertion-error": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/axios": {
      "version": "1.13.6",
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "follow-redirects": "^1.15.11",
        "form-data": "^4.0.5",
        "proxy-from-env": "^1.1.0"
      }
    },
    "node_modules/base-x": {
      "version": "3.0.11",
      "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safe-buffer": "^5.0.1"
      }
    },
    "node_modules/base64-js": {
      "version": "1.5.1",
      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/better-sqlite3": {
      "version": "11.10.0",
      "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
      "integrity": [REDACTED_SECRET],
      "hasInstallScript": true,
      "license": "MIT",
      "dependencies": {
        "bindings": "^1.5.0",
        "prebuild-install": "^7.1.1"
      }
    },
    "node_modules/bindings": {
      "version": "1.5.0",
      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "file-uri-to-path": "1.0.0"
      }
    },
    "node_modules/bl": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "buffer": "^5.5.0",
        "inherits": "^2.0.4",
        "readable-stream": "^3.4.0"
      }
    },
    "node_modules/buffer": {
      "version": "5.7.1",
      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "base64-js": "^1.3.1",
        "ieee754": "^1.1.13"
      }
    },
    "node_modules/cac": {
      "version": "6.7.14",
      "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/chai": {
      "version": "5.3.3",
      "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "assertion-error": "^2.0.1",
        "check-error": "^2.1.1",
        "deep-eql": "^5.0.1",
        "loupe": "^3.1.0",
        "pathval": "^2.0.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/check-error": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 16"
      }
    },
    "node_modules/chownr": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/core-js": {
      "version": "3.48.0",
      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
      "integrity": [REDACTED_SECRET],
      "hasInstallScript": true,
      "license": "MIT",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/core-js"
      }
    },
    "node_modules/core-js-pure": {
      "version": "3.48.0",
      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz",
      "integrity": [REDACTED_SECRET],
      "hasInstallScript": true,
      "license": "MIT",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/core-js"
      }
    },
    "node_modules/crypto-js": {
      "version": "4.2.0",
      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/decimal.js": {
      "version": "10.6.0",
      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/decompress-response": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mimic-response": "^3.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/deep-eql": {
      "version": "5.0.2",
      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/deep-extend": {
      "version": "0.6.0",
      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=4.0.0"
      }
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/detect-libc": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ed2curve": {
      "version": "0.3.0",
      "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Unlicense",
      "dependencies": {
        "tweetnacl": "1.x.x"
      }
    },
    "node_modules/end-of-stream": {
      "version": "1.4.5",
      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "once": "^1.4.0"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-module-lexer": {
      "version": "1.7.0",
      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/esbuild": {
      "version": "0.21.5",
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "bin": {
        "esbuild": "bin/esbuild"
      },
      "engines": {
        "node": ">=12"
      },
      "optionalDependencies": {
        "@esbuild/aix-ppc64": "0.21.5",
        "@esbuild/android-arm": "0.21.5",
        "@esbuild/android-arm64": "0.21.5",
        "@esbuild/android-x64": "0.21.5",
        "@esbuild/darwin-arm64": "0.21.5",
        "@esbuild/darwin-x64": "0.21.5",
        "@esbuild/freebsd-arm64": "0.21.5",
        "@esbuild/freebsd-x64": "0.21.5",
        "@esbuild/linux-arm": "0.21.5",
        "@esbuild/linux-arm64": "0.21.5",
        "@esbuild/linux-ia32": "0.21.5",
        "@esbuild/linux-loong64": "0.21.5",
        "@esbuild/linux-mips64el": "0.21.5",
        "@esbuild/linux-ppc64": "0.21.5",
        "@esbuild/linux-riscv64": "0.21.5",
        "@esbuild/linux-s390x": "0.21.5",
        "@esbuild/linux-x64": "0.21.5",
        "@esbuild/netbsd-x64": "0.21.5",
        "@esbuild/openbsd-x64": "0.21.5",
        "@esbuild/sunos-x64": "0.21.5",
        "@esbuild/win32-arm64": "0.21.5",
        "@esbuild/win32-ia32": "0.21.5",
        "@esbuild/win32-x64": "0.21.5"
      }
    },
    "node_modules/estree-walker": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/estree": "^1.0.0"
      }
    },
    "node_modules/expand-template": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "(MIT OR WTFPL)",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/expect-type": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": ">=12.0.0"
      }
    },
    "node_modules/file-uri-to-path": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/follow-redirects": {
      "version": "1.15.11",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/RubenVerborgh"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=4.0"
      },
      "peerDependenciesMeta": {
        "debug": {
          "optional": true
        }
      }
    },
    "node_modules/form-data": {
      "version": "4.0.5",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/fs-constants": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/get-tsconfig": {
      "version": "4.13.6",
      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "resolve-pkg-maps": "^1.0.0"
      },
      "funding": {
        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
      }
    },
    "node_modules/github-from-package": {
      "version": "0.0.0",
      "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/google-protobuf": {
      "version": "3.21.4",
      "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "(BSD-3-Clause AND Apache-2.0)"
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/heap": {
      "version": "0.2.7",
      "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/ieee754": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ini": {
      "version": "1.3.8",
      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/isomorphic-ws": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "peerDependencies": {
        "ws": "*"
      }
    },
    "node_modules/libsodium": {
      "version": "0.7.16",
      "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.16.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/libsodium-wrappers": {
      "version": "0.7.16",
      "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.16.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "libsodium": "^0.7.16"
      }
    },
    "node_modules/loupe": {
      "version": "3.2.1",
      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/magic-string": {
      "version": "0.30.21",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@jridgewell/sourcemap-codec": "^1.5.5"
      }
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/memory-cache": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause"
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mimic-response": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/minimist": {
      "version": "1.2.8",
      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/mkdirp-classic": {
      "version": "0.5.3",
      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/nanoid": {
      "version": "3.3.11",
      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "bin": {
        "nanoid": "bin/nanoid.cjs"
      },
      "engines": {
        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
      }
    },
    "node_modules/napi-build-utils": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/nkn-sdk": {
      "version": "1.3.6",
      "resolved": "https://registry.npmjs.org/nkn-sdk/-/nkn-sdk-1.3.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@babel/polyfill": "^7.12.1",
        "@nkn/ncp": "^1.1.2",
        "axios": "^1.8.4",
        "base-x": "^3.0.7",
        "buffer": "^5.4.3",
        "core-js": "^3.41.0",
        "core-js-pure": "^3.6.4",
        "crypto-js": "^4.2.0",
        "decimal.js": "^10.2.0",
        "ed2curve": "^0.3.0",
        "google-protobuf": "^3.11.2",
        "heap": "^0.2.6",
        "isomorphic-ws": "^4.0.1",
        "libsodium-wrappers": "^0.7.6",
        "memory-cache": "^0.2.0",
        "pako": "^1.0.11",
        "scrypt-js": "^3.0.0",
        "tweetnacl": "^1.0.3",
        "webworkify": "^1.5.0",
        "ws": "^7.2.3"
      }
    },
    "node_modules/node-abi": {
      "version": "3.89.0",
      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "semver": "^7.3.5"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/pako": {
      "version": "1.0.11",
      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "(MIT AND Zlib)"
    },
    "node_modules/pathe": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/pathval": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 14.16"
      }
    },
    "node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/postcss": {
      "version": "8.5.8",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/postcss/"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/postcss"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "nanoid": "^3.3.11",
        "picocolors": "^1.1.1",
        "source-map-js": "^1.2.1"
      },
      "engines": {
        "node": "^10 || ^12 || >=14"
      }
    },
    "node_modules/prebuild-install": {
      "version": "7.1.3",
      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
      "license": "MIT",
      "dependencies": {
        "detect-libc": "^2.0.0",
        "expand-template": "^2.0.3",
        "github-from-package": "0.0.0",
        "minimist": "^1.2.3",
        "mkdirp-classic": "^0.5.3",
        "napi-build-utils": "^2.0.0",
        "node-abi": "^3.3.0",
        "pump": "^3.0.0",
        "rc": "^1.2.7",
        "simple-get": "^4.0.0",
        "tar-fs": "^2.0.0",
        "tunnel-agent": "^0.6.0"
      },
      "bin": {
        "prebuild-install": "bin.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/proxy-from-env": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/pump": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "end-of-stream": "^1.1.0",
        "once": "^1.3.1"
      }
    },
    "node_modules/rc": {
      "version": "1.2.8",
      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
      "dependencies": {
        "deep-extend": "^0.6.0",
        "ini": "~1.3.0",
        "minimist": "^1.2.0",
        "strip-json-comments": "~2.0.1"
      },
      "bin": {
        "rc": "cli.js"
      }
    },
    "node_modules/readable-stream": {
      "version": "3.6.2",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "inherits": "^2.0.3",
        "string_decoder": "^1.1.1",
        "util-deprecate": "^1.0.1"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/regenerator-runtime": {
      "version": "0.13.11",
      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/resolve-pkg-maps": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
      }
    },
    "node_modules/rollup": {
      "version": "4.59.0",
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/estree": "1.0.8"
      },
      "bin": {
        "rollup": "dist/bin/rollup"
      },
      "engines": {
        "node": ">=18.0.0",
        "npm": ">=8.0.0"
      },
      "optionalDependencies": {
        "@rollup/rollup-android-arm-eabi": "4.59.0",
        "@rollup/rollup-android-arm64": "4.59.0",
        "@rollup/rollup-darwin-arm64": "4.59.0",
        "@rollup/rollup-darwin-x64": "4.59.0",
        "@rollup/rollup-freebsd-arm64": "4.59.0",
        "@rollup/rollup-freebsd-x64": "4.59.0",
        "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
        "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
        "@rollup/rollup-linux-arm64-gnu": "4.59.0",
        "@rollup/rollup-linux-arm64-musl": "4.59.0",
        "@rollup/rollup-linux-loong64-gnu": "4.59.0",
        "@rollup/rollup-linux-loong64-musl": "4.59.0",
        "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
        "@rollup/rollup-linux-ppc64-musl": "4.59.0",
        "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
        "@rollup/rollup-linux-riscv64-musl": "4.59.0",
        "@rollup/rollup-linux-s390x-gnu": "4.59.0",
        "@rollup/rollup-linux-x64-gnu": "4.59.0",
        "@rollup/rollup-linux-x64-musl": "4.59.0",
        "@rollup/rollup-openbsd-x64": "4.59.0",
        "@rollup/rollup-openharmony-arm64": "4.59.0",
        "@rollup/rollup-win32-arm64-msvc": "4.59.0",
        "@rollup/rollup-win32-ia32-msvc": "4.59.0",
        "@rollup/rollup-win32-x64-gnu": "4.59.0",
        "@rollup/rollup-win32-x64-msvc": "4.59.0",
        "fsevents": "~2.3.2"
      }
    },
    "node_modules/safe-buffer": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/scrypt-js": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/semver": {
      "version": "7.7.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/setimmediate": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/siginfo": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "ISC"
    },
    "node_modules/simple-concat": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/simple-get": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "decompress-response": "^6.0.0",
        "once": "^1.3.1",
        "simple-concat": "^1.0.0"
      }
    },
    "node_modules/source-map-js": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/stackback": {
      "version": "0.0.2",
      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/std-env": {
      "version": "3.10.0",
      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/string_decoder": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safe-buffer": "~5.2.0"
      }
    },
    "node_modules/strip-json-comments": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/tar-fs": {
      "version": "2.1.4",
      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "chownr": "^1.1.1",
        "mkdirp-classic": "^0.5.2",
        "pump": "^3.0.0",
        "tar-stream": "^2.1.4"
      }
    },
    "node_modules/tar-stream": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bl": "^4.0.3",
        "end-of-stream": "^1.4.1",
        "fs-constants": "^1.0.0",
        "inherits": "^2.0.3",
        "readable-stream": "^3.1.1"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/tinybench": {
      "version": "2.9.0",
      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/tinyexec": {
      "version": "0.3.2",
      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/tinypool": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": "^18.0.0 || >=20.0.0"
      }
    },
    "node_modules/tinyrainbow": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/tinyspy": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/tsx": {
      "version": "4.21.0",
      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "esbuild": "~0.27.0",
        "get-tsconfig": "^4.7.5"
      },
      "bin": {
        "tsx": "dist/cli.mjs"
      },
      "engines": {
        "node": ">=18.0.0"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.3"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "aix"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/android-arm": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/android-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/android-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/linux-arm": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "mips64el"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/linux-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "sunos"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/@esbuild/win32-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/tsx/node_modules/esbuild": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "bin": {
        "esbuild": "bin/esbuild"
      },
      "engines": {
        "node": ">=18"
      },
      "optionalDependencies": {
        "@esbuild/aix-ppc64": "0.27.4",
        "@esbuild/android-arm": "0.27.4",
        "@esbuild/android-arm64": "0.27.4",
        "@esbuild/android-x64": "0.27.4",
        "@esbuild/darwin-arm64": "0.27.4",
        "@esbuild/darwin-x64": "0.27.4",
        "@esbuild/freebsd-arm64": "0.27.4",
        "@esbuild/freebsd-x64": "0.27.4",
        "@esbuild/linux-arm": "0.27.4",
        "@esbuild/linux-arm64": "0.27.4",
        "@esbuild/linux-ia32": "0.27.4",
        "@esbuild/linux-loong64": "0.27.4",
        "@esbuild/linux-mips64el": "0.27.4",
        "@esbuild/linux-ppc64": "0.27.4",
        "@esbuild/linux-riscv64": "0.27.4",
        "@esbuild/linux-s390x": "0.27.4",
        "@esbuild/linux-x64": "0.27.4",
        "@esbuild/netbsd-arm64": "0.27.4",
        "@esbuild/netbsd-x64": "0.27.4",
        "@esbuild/openbsd-arm64": "0.27.4",
        "@esbuild/openbsd-x64": "0.27.4",
        "@esbuild/openharmony-arm64": "0.27.4",
        "@esbuild/sunos-x64": "0.27.4",
        "@esbuild/win32-arm64": "0.27.4",
        "@esbuild/win32-ia32": "0.27.4",
        "@esbuild/win32-x64": "0.27.4"
      }
    },
    "node_modules/tunnel-agent": {
      "version": "0.6.0",
      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "safe-buffer": "^5.0.1"
      },
      "engines": {
        "node": "*"
      }
    },
    "node_modules/tweetnacl": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Unlicense"
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "6.21.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/util-deprecate": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/vite": {
      "version": "5.4.21",
      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "esbuild": "^0.21.3",
        "postcss": "^8.4.43",
        "rollup": "^4.20.0"
      },
      "bin": {
        "vite": "bin/vite.js"
      },
      "engines": {
        "node": "^18.0.0 || >=20.0.0"
      },
      "funding": {
        "url": "https://github.com/vitejs/vite?sponsor=1"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.3"
      },
      "peerDependencies": {
        "@types/node": "^18.0.0 || >=20.0.0",
        "less": "*",
        "lightningcss": "^1.21.0",
        "sass": "*",
        "sass-embedded": "*",
        "stylus": "*",
        "sugarss": "*",
        "terser": "^5.4.0"
      },
      "peerDependenciesMeta": {
        "@types/node": {
          "optional": true
        },
        "less": {
          "optional": true
        },
        "lightningcss": {
          "optional": true
        },
        "sass": {
          "optional": true
        },
        "sass-embedded": {
          "optional": true
        },
        "stylus": {
          "optional": true
        },
        "sugarss": {
          "optional": true
        },
        "terser": {
          "optional": true
        }
      }
    },
    "node_modules/vite-node": {
      "version": "2.1.9",
      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "cac": "^6.7.14",
        "debug": "^4.3.7",
        "es-module-lexer": "^1.5.4",
        "pathe": "^1.1.2",
        "vite": "^5.0.0"
      },
      "bin": {
        "vite-node": "vite-node.mjs"
      },
      "engines": {
        "node": "^18.0.0 || >=20.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      }
    },
    "node_modules/vitest": {
      "version": "2.1.9",
      "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@vitest/expect": "2.1.9",
        "@vitest/mocker": "2.1.9",
        "@vitest/pretty-format": "^2.1.9",
        "@vitest/runner": "2.1.9",
        "@vitest/snapshot": "2.1.9",
        "@vitest/spy": "2.1.9",
        "@vitest/utils": "2.1.9",
        "chai": "^5.1.2",
        "debug": "^4.3.7",
        "expect-type": "^1.1.0",
        "magic-string": "^0.30.12",
        "pathe": "^1.1.2",
        "std-env": "^3.8.0",
        "tinybench": "^2.9.0",
        "tinyexec": "^0.3.1",
        "tinypool": "^1.0.1",
        "tinyrainbow": "^1.2.0",
        "vite": "^5.0.0",
        "vite-node": "2.1.9",
        "why-is-node-running": "^2.3.0"
      },
      "bin": {
        "vitest": "vitest.mjs"
      },
      "engines": {
        "node": "^18.0.0 || >=20.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/vitest"
      },
      "peerDependencies": {
        "@edge-runtime/vm": "*",
        "@types/node": "^18.0.0 || >=20.0.0",
        "@vitest/browser": "2.1.9",
        "@vitest/ui": "2.1.9",
        "happy-dom": "*",
        "jsdom": "*"
      },
      "peerDependenciesMeta": {
        "@edge-runtime/vm": {
          "optional": true
        },
        "@types/node": {
          "optional": true
        },
        "@vitest/browser": {
          "optional": true
        },
        "@vitest/ui": {
          "optional": true
        },
        "happy-dom": {
          "optional": true
        },
        "jsdom": {
          "optional": true
        }
      }
    },
    "node_modules/webworkify": {
      "version": "1.5.0",
      "resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/why-is-node-running": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "siginfo": "^2.0.0",
        "stackback": "0.0.2"
      },
      "bin": {
        "why-is-node-running": "cli.js"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ws": {
      "version": "7.5.10",
      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8.3.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": "dchat-clawhub",
  "version": "0.1.0",
  "description": "Decentralized bot-to-bot communication over NKN — a ClawHub skill for private, serverless P2P messaging",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "bin": {
    "dchat-bot": "dist/cli.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "start": "node dist/cli.js",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:e2e": "npx tsx tests/e2e-self-echo.ts",
    "typecheck": "tsc --noEmit",
    "lint": "eslint src/",
    "clean": "rm -rf dist"
  },
  "dependencies": {
    "better-sqlite3": "^11.7.0",
    "form-data": "^4.0.5",
    "nkn-sdk": "^1.3.6"
  },
  "devDependencies": {
    "@types/better-sqlite3": "^7.6.12",
    "@types/node": "^22.0.0",
    "tsx": "^4.19.0",
    "typescript": "^5.7.0",
    "vitest": "^2.0.0"
  },
  "engines": {
    "node": ">=20.0.0"
  }
}


path: src/bot.ts
content: import crypto from "crypto";
import path from "path";
import os from "os";
import { EventEmitter } from "events";
import { NknClient } from "./client.js";
import { IpfsService } from "./ipfs.js";
import { MediaService } from "./media.js";
import { MessageDb } from "./db.js";
import type {
  BotConfig,
  MessageData,
  MessageOptions,
  MessageContentType,
  IncomingMessage,
  ClientStatus,
  Message,
} from "./types.js";

const DISPLAYABLE_TYPES = new Set<MessageContentType>([
  "text",
  "textExtension",
  "image",
  "audio",
  "file",
]);

/**
 * DchatBot — headless, CLI-friendly P2P bot over NKN.
 *
 * Events:
 *   "message"      — (msg: IncomingMessage) — text, image, audio, or file received
 *   "receipt"       — (from: string, messageId: string) — delivery receipt
 *   "readReceipt"   — (from: string, messageIds: string[]) — read receipt
 *   "connected"     — (status: ClientStatus) — NKN client connected
 *   "disconnected"  — () — NKN client disconnected
 */
export class DchatBot extends EventEmitter {
  private nkn: NknClient;
  private ipfs: IpfsService;
  private media: MediaService;
  private db: MessageDb;
  private config: Required<BotConfig>;
  private seed: string;
  private burnTimer: ReturnType<typeof setInterval> | null = null;

  constructor(config: BotConfig = {}) {
    super();
    this.seed = config.seed ?? NknClient.generateSeed();
    this.config = {
      seed: this.seed,
      dataDir: config.dataDir ?? path.join(os.homedir(), ".dchat-clawhub"),
      ipfsGateways: config.ipfsGateways ?? [],
      numSubClients: config.numSubClients ?? 4,
      autoDownloadMedia: config.autoDownloadMedia ?? true,
    };

    this.nkn = new NknClient(this.config.numSubClients);
    this.ipfs = new IpfsService(this.config.ipfsGateways.length ? this.config.ipfsGateways : undefined);
    this.media = new MediaService(this.ipfs, this.config.dataDir);
    this.db = new MessageDb(this.config.dataDir);

    this.nkn.on("message", (src: string, payload: string) => {
      this.handleIncoming(src, payload).catch((err) =>
        console.error("[dchat-bot] Error handling message:", err),
      );
    });

    this.nkn.on("statusChange", (status: ClientStatus) => {
      if (status.state === "connected") this.emit("connected", status);
      if (status.state === "disconnected") this.emit("disconnected");
    });
  }

  /** Connect to NKN network. Returns bot's NKN address. */
  async start(): Promise<string> {
    const status = await this.nkn.connect(this.seed);
    this.startBurnScheduler();
    return status.address!;
  }

  /** Disconnect and clean up. */
  async stop(): Promise<void> {
    this.stopBurnScheduler();
    await this.nkn.disconnect();
    this.db.close();
  }

  /** Get the bot's NKN address (available after start). */
  get address(): string | undefined {
    return this.nkn.getAddress();
  }

  /** Get the seed (for persistence). */
  getSeed(): string {
    return this.seed;
  }

  // ── Sending ──────────────────────────────────────────────

  /**
   * Send a text message (fire-and-forget). Returns message ID.
   * Use for long-running bots. Delivery confirmation arrives via receipt events.
   */
  sendText(to: string, text: string): string {
    const msg = this.buildMessage("text", text);
    this.sendAndStoreNoReply(to, msg);
    return msg.id;
  }

  /**
   * Send a text message and wait for NKN network to accept it.
   * Use for one-shot CLI sends where the process exits immediately after.
   * Returns message ID. Does NOT wait for recipient ACK — only for relay dispatch.
   * If the recipient is offline, the message is queued by NKN relay nodes.
   */
  async sendTextAwait(to: string, text: string): Promise<string> {
    const msg = this.buildMessage("text", text);
    const myAddress = this.nkn.getAddress()!;
    this.storeOutbound(myAddress, to, msg, "sending");
    try {
      await this.nkn.send(to, JSON.stringify(msg));
      this.db.updateStatusIfNotDelivered(msg.id, "sent");
    } catch (err) {
      const errMsg = err instanceof Error ? err.message : String(err);
      // Any timeout (including "failed to send with any client: ... Message timeout")
      // means the NKN relay accepted the message but recipient didn't ACK (offline).
      // The relay still queues the message for msgHoldingSeconds (1 hour).
      if (errMsg.toLowerCase().includes("timeout")) {
        console.log("[dchat] Recipient offline — message queued by NKN relay (up to 1 hour)");
        this.db.updateStatusIfNotDelivered(msg.id, "sent");
      } else {
        this.db.updateStatus(msg.id, "failed");
        throw err;
      }
    }
    return msg.id;
  }

  /** Send an image file. Returns message ID. */
  async sendImage(to: string, filePath: string): Promise<string> {
    const result = await this.media.uploadImage(filePath);
    const msg = this.buildMessage("image", result.content, result.options);
    this.sendAndStoreNoReply(to, msg, result.localFilePath);
    return msg.id;
  }

  /** Send an audio file. Returns message ID. */
  async sendAudio(to: string, filePath: string, durationSeconds?: number): Promise<string> {
    const result = await this.media.uploadAudio(filePath, durationSeconds);
    const msg = this.buildMessage("audio", result.content, result.options);
    this.sendAndStoreNoReply(to, msg, result.localFilePath);
    return msg.id;
  }

  /** Send a generic file. Returns message ID. */
  async sendFile(to: string, filePath: string): Promise<string> {
    const result = await this.media.uploadFile(filePath);
    const msg = this.buildMessage("file", result.content, result.options);
    this.sendAndStoreNoReply(to, msg, result.localFilePath);
    return msg.id;
  }

  /** Send a delivery receipt for a received message. */
  sendReceipt(to: string, messageId: string): void {
    const msg: MessageData = {
      id: crypto.randomUUID(),
      contentType: "receipt",
      targetID: messageId,
      timestamp: Date.now(),
    };
    this.nkn.sendNoReply(to, JSON.stringify(msg));
  }

  /** Send read receipts for received messages. */
  sendReadReceipt(to: string, messageIds: string[]): void {
    const msg: MessageData = {
      id: crypto.randomUUID(),
      contentType: "read",
      readIds: messageIds,
      timestamp: Date.now(),
    };
    this.nkn.sendNoReply(to, JSON.stringify(msg));
  }

  // ── History ──────────────────────────────────────────────

  /** Get message history with a peer. */
  getMessages(peerAddress: string, limit = 50, before?: number): Message[] {
    return this.db.getMessages(peerAddress, limit, before);
  }

  // ── Internal ─────────────────────────────────────────────

  private buildMessage(
    contentType: MessageContentType,
    content: string,
    options?: MessageOptions,
  ): MessageData {
    return {
      id: crypto.randomUUID(),
      contentType,
      content,
      options,
      timestamp: Date.now(),
    };
  }

  private async sendAndStore(to: string, msg: MessageData, localFilePath?: string): Promise<void> {
    const myAddress = this.nkn.getAddress()!;
    this.storeOutbound(myAddress, to, msg, "sending", localFilePath);
    try {
      await this.nkn.send(to, JSON.stringify(msg));
      // Only upgrade status — don't downgrade if already "delivered" (self-echo race)
      this.db.updateStatusIfNotDelivered(msg.id, "sent");
    } catch (err) {
      this.db.updateStatus(msg.id, "failed");
      throw err;
    }
  }

  private sendAndStoreNoReply(to: string, msg: MessageData, localFilePath?: string): void {
    const myAddress = this.nkn.getAddress()!;
    this.storeOutbound(myAddress, to, msg, "sent", localFilePath);
    this.nkn.sendNoReply(to, JSON.stringify(msg));
  }

  private storeOutbound(
    from: string,
    to: string,
    msg: MessageData,
    status: "sending" | "sent",
    localFilePath?: string,
  ): void {
    this.db.insertMessage({
      id: msg.id,
      sender: from,
      receiver: to,
      contentType: msg.contentType,
      content: msg.content ?? "",
      status,
      isOutbound: true,
      options: msg.options ? JSON.stringify(msg.options) : undefined,
      localFilePath,
      createdAt: msg.timestamp,
    });
  }

  private async handleIncoming(src: string, payload: string): Promise<void> {
    let msgData: MessageData;
    try {
      msgData = JSON.parse(payload);
    } catch {
      // Try base64 decode (nMobile sometimes sends base64)
      try {
        msgData = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
      } catch {
        return; // Unparseable — ignore
      }
    }

    if (!msgData.id || !msgData.contentType) return;

    // Handle receipts
    if (msgData.contentType === "receipt" && msgData.targetID) {
      this.db.updateStatus(msgData.targetID, "delivered");
      this.emit("receipt", src, msgData.targetID);
      return;
    }

    if (msgData.contentType === "read" && msgData.readIds) {
      for (const id of msgData.readIds) {
        this.db.updateStatus(id, "read");
      }
      this.emit("readReceipt", src, msgData.readIds);
      return;
    }

    // Skip non-displayable control messages
    if (!DISPLAYABLE_TYPES.has(msgData.contentType)) return;

    const myAddress = this.nkn.getAddress()!;
    const isSelfMessage = src === myAddress;

    // Dedup — but allow self-echo (outbound already stored, inbound is the echo)
    if (this.db.hasMessage(msgData.id)) {
      if (!isSelfMessage) return;
      // Self-echo: update outbound status to delivered, emit event, but don't re-store
      this.db.updateStatus(msgData.id, "delivered");
    } else {
      // Store inbound message
      this.db.insertMessage({
        id: msgData.id,
        sender: src,
        receiver: myAddress,
        contentType: msgData.contentType,
        content: msgData.content ?? "",
        status: "delivered",
        isOutbound: false,
        options: msgData.options ? JSON.stringify(msgData.options) : undefined,
        createdAt: msgData.timestamp || Date.now(),
      });
    }

    // Track peer (skip self)
    if (!isSelfMessage) this.db.upsertPeer(src);

    // Send delivery receipt (skip self — avoids receipt loop)
    if (!isSelfMessage) this.sendReceipt(src, msgData.id);

    // Build event payload
    const incoming: IncomingMessage = {
      id: msgData.id,
      from: src,
      contentType: msgData.contentType,
      content: msgData.content ?? "",
      options: msgData.options,
      timestamp: msgData.timestamp || Date.now(),
    };

    // Auto-download media
    if (this.config.autoDownloadMedia && msgData.options) {
      try {
        const localPath = await this.downloadMediaFromOptions(
          msgData.id,
          msgData.contentType,
          msgData.content ?? "",
          msgData.options,
        );
        if (localPath) {
          incoming.localFilePath = localPath;
          this.db.updateLocalFilePath(msgData.id, localPath);
        }
      } catch (err) {
        console.error(`[dchat-bot] Failed to download media for ${msgData.id}:`, err);
      }
    }

    this.emit("message", incoming);
  }

  private async downloadMediaFromOptions(
    messageId: string,
    contentType: MessageContentType,
    content: string,
    options: MessageOptions,
  ): Promise<string | null> {
    // Inline audio (base64 data-URI)
    if (contentType === "audio" && content.startsWith("![audio]")) {
      return this.media.saveInlineAudio(messageId, content, (options.fileExt as string) ?? "aac");
    }

    // IPFS media
    if (options.ipfsHash && options.ipfsEncryptKeyBytes) {
      return this.media.downloadMedia(
        options.ipfsHash,
        options.ipfsEncryptKeyBytes,
        options.ipfsEncryptNonceSize ?? 12,
        (options.fileExt as string) ?? "bin",
        options.ipfsIp,
      );
    }

    return null;
  }

  private startBurnScheduler(): void {
    if (this.burnTimer) return;
    this.burnTimer = setInterval(() => {
      try {
        this.db.burnExpired();
      } catch (err) {
        console.error("[dchat-bot] Burn scheduler error:", err);
      }
    }, 5000);
  }

  private stopBurnScheduler(): void {
    if (this.burnTimer) {
      clearInterval(this.burnTimer);
      this.burnTimer = null;
    }
  }
}


path: src/cli.ts
content: #!/usr/bin/env node

import crypto from "crypto";
import fs from "fs";
import path from "path";
import os from "os";
import readline from "readline";
import { DchatBot } from "./bot.js";
import { NknClient } from "./client.js";
import { SafeStorage } from "./storage.js";
import { MessageDb } from "./db.js";
import type { IncomingMessage } from "./types.js";

const DATA_DIR = path.join(os.homedir(), ".dchat-clawhub");

// ── Identity helpers ───────────────────────────────────────

/**
 * Get or create a random passphrase for encrypting the identity at rest.
 *
 * The passkey is stored in a SEPARATE directory from the encrypted identity
 * (OS config dir vs data dir) so the encryption key is not alongside the
 * ciphertext. This means a leak of ~/.dchat-clawhub/identity.enc alone
 * does not expose the seed.
 *
 * Passkey location: ~/.config/dchat-clawhub/.passkey (mode 0600)
 * Identity location: ~/.dchat-clawhub/identity.enc (mode 0600)
 */
function getPassphrase(dataDir: string): string {
  // Use XDG_CONFIG_HOME or platform-appropriate config dir, separate from dataDir
  const configBase = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
  const configDir = path.join(configBase, "dchat-clawhub");
  const keyFile = path.join(configDir, ".passkey");

  // Migration: move legacy .passkey from dataDir if present
  const legacyKeyFile = path.join(dataDir, ".passkey");
  if (fs.existsSync(legacyKeyFile) && !fs.existsSync(keyFile)) {
    fs.mkdirSync(configDir, { recursive: true });
    fs.renameSync(legacyKeyFile, keyFile);
    fs.chmodSync(keyFile, 0o600);
    return fs.readFileSync(keyFile, "utf-8").trim();
  }

  if (fs.existsSync(keyFile)) {
    return fs.readFileSync(keyFile, "utf-8").trim();
  }

  const passphrase = crypto.randomBytes(32).toString("base64url");
  fs.mkdirSync(configDir, { recursive: true });
  fs.writeFileSync(keyFile, passphrase, { mode: 0o600 });
  return passphrase;
}

function loadOrCreateIdentity(dataDir: string): { seed: string; address: string; isNew: boolean } {
  fs.mkdirSync(dataDir, { recursive: true });
  const storage = new SafeStorage(dataDir);
  const passphrase = getPassphrase(dataDir);

  if (storage.exists()) {
    const identity = storage.load(passphrase);
    if (identity) return { ...identity, isNew: false };
  }

  // Migrate legacy plaintext seed
  const legacySeedFile = path.join(dataDir, "seed");
  if (fs.existsSync(legacySeedFile)) {
    const seed = fs.readFileSync(legacySeedFile, "utf-8").trim();
    const address = NknClient.deriveAddress(seed);
    storage.save({ seed, address }, passphrase);
    fs.unlinkSync(legacySeedFile);
    return { seed, address, isNew: false };
  }

  const seed = NknClient.generateSeed();
  const address = NknClient.deriveAddress(seed);
  storage.save({ seed, address }, passphrase);
  return { seed, address, isNew: true };
}

// ── Sub-command handlers ───────────────────────────────────

function cmdHelp() {
  console.log(`dchat — Decentralized P2P bot communication over NKN

Commands:
  dchat init                          Generate bot identity (no network needed)
  dchat address                       Print bot's NKN address (no network needed)
  dchat send <to> <message>           Send a text message
  dchat send-image <to> <path>        Send an image
  dchat send-audio <to> <path>        Send an audio file
  dchat send-file <to> <path>         Send a file
  dchat history <peer> [limit]        Show message history with a peer
  dchat contacts add <address> <alias>  Add a contact with alias
  dchat contacts remove <alias>         Remove a contact
  dchat contacts list                   List all contacts
  dchat listen                        Listen for incoming messages (daemon)
  dchat interactive                   Start interactive REPL
  dchat help                          Show this help

<to> and <peer> accept either a 64-char NKN address or a contact alias.

Options:
  --seed <hex>          Use specific seed (overrides stored identity)
  --data-dir <path>     Custom data directory (default: ~/.dchat-clawhub)

Data stored in: ${DATA_DIR}`);
}

/** Resolve an alias or address to an NKN address using the contacts DB. */
function resolveAddress(dataDir: string, addressOrAlias: string): string {
  if (/^[0-9a-f]{64}$/i.test(addressOrAlias)) return addressOrAlias;
  const db = new MessageDb(dataDir);
  const resolved = db.resolveAlias(addressOrAlias);
  db.close();
  if (!resolved) {
    console.error(`Unknown contact: "${addressOrAlias}". Use 'dchat contacts list' to see contacts.`);
    process.exit(1);
  }
  return resolved;
}

function cmdInit(dataDir: string) {
  const identity = loadOrCreateIdentity(dataDir);
  if (identity.isNew) {
    console.log("Generated new bot identity.");
  } else {
    console.log("Bot identity already exists.");
  }
  console.log(`Address: ${identity.address}`);
  console.log(`Data dir: ${dataDir}`);
}

function cmdAddress(dataDir: string, explicitSeed?: string) {
  if (explicitSeed) {
    console.log(NknClient.deriveAddress(explicitSeed));
    return;
  }
  const identity = loadOrCreateIdentity(dataDir);
  console.log(identity.address);
}

async function cmdSend(dataDir: string, seed: string, target: string, message: string) {
  const bot = new DchatBot({ seed, dataDir });
  console.log("Connecting...");
  await bot.start();
  // Wait for all sub-clients to establish relay routes
  await new Promise((r) => setTimeout(r, 3000));
  const id = await bot.sendTextAwait(target, message);
  console.log(`Sent: ${id}`);
  process.exit(0);
}

async function cmdSendMedia(
  dataDir: string,
  seed: string,
  type: "image" | "audio" | "file",
  target: string,
  filePath: string,
) {
  const bot = new DchatBot({ seed, dataDir });
  console.log("Connecting...");
  await bot.start();
  await new Promise((r) => setTimeout(r, 3000));
  let id: string;
  switch (type) {
    case "image":
      id = await bot.sendImage(target, filePath);
      break;
    case "audio":
      id = await bot.sendAudio(target, filePath);
      break;
    case "file":
      id = await bot.sendFile(target, filePath);
      break;
  }
  console.log(`Sent ${type}: ${id}`);
  process.exit(0);
}

function cmdHistory(dataDir: string, seed: string, peerAddress: string, limit: number) {
  const bot = new DchatBot({ seed, dataDir });
  const msgs = bot.getMessages(peerAddress, limit);
  if (msgs.length === 0) {
    console.log("No messages.");
  } else {
    for (const m of msgs) {
      const dir = m.isOutbound ? "→" : "←";
      const time = new Date(m.createdAt).toISOString().substring(11, 19);
      const preview = m.content.length > 80 ? m.content.substring(0, 80) + "..." : m.content;
      console.log(`${dir} [${time}] (${m.contentType}) ${preview}`);
    }
  }
  bot.stop().catch(() => {});
}

async function cmdListen(dataDir: string, seed: string) {
  const bot = new DchatBot({ seed, dataDir });
  const contactDb = new MessageDb(dataDir);
  const shutdown = async () => {
    console.log("\nShutting down...");
    contactDb.close();
    await bot.stop();
    process.exit(0);
  };
  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);

  bot.on("message", (msg: IncomingMessage) => {
    const time = new Date(msg.timestamp).toISOString().substring(11, 19);
    const alias = contactDb.getAlias(msg.from);
    const from = alias ?? msg.from.substring(0, 16) + "...";
    switch (msg.contentType) {
      case "text":
      case "textExtension":
        console.log(`[${time}] ${from}: ${msg.content}`);
        break;
      case "image":
        console.log(`[${time}] ${from}: [image] ${msg.localFilePath ?? msg.content}`);
        break;
      case "audio":
        console.log(`[${time}] ${from}: [audio] ${msg.localFilePath ?? "(inline)"}`);
        break;
      case "file": {
        const fileName = msg.options?.fileName ?? "file";
        console.log(`[${time}] ${from}: [file: ${fileName}] ${msg.localFilePath ?? msg.content}`);
        break;
      }
    }
  });

  console.log("Connecting...");
  const address = await bot.start();
  console.log(`Listening as ${address} (Ctrl+C to exit)`);
}

async function cmdInteractive(dataDir: string, seed: string) {
  const bot = new DchatBot({ seed, dataDir });
  const contactDb = new MessageDb(dataDir);
  const shutdown = async () => {
    console.log("\nShutting down...");
    contactDb.close();
    await bot.stop();
    process.exit(0);
  };
  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);

  bot.on("message", (msg: IncomingMessage) => {
    const time = new Date(msg.timestamp).toISOString().substring(11, 19);
    const alias = contactDb.getAlias(msg.from);
    const from = alias ?? msg.from.substring(0, 16) + "...";
    switch (msg.contentType) {
      case "text":
      case "textExtension":
        console.log(`\n[${time}] ${from}: ${msg.content}`);
        break;
      case "image":
        console.log(`\n[${time}] ${from}: [image] ${msg.localFilePath ?? msg.content}`);
        break;
      case "audio":
        console.log(`\n[${time}] ${from}: [audio] ${msg.localFilePath ?? "(inline)"}`);
        break;
      case "file": {
        const fileName = msg.options?.fileName ?? "file";
        console.log(`\n[${time}] ${from}: [file: ${fileName}] ${msg.localFilePath ?? msg.content}`);
        break;
      }
    }
    rl.prompt();
  });

  console.log("Connecting...");
  const address = await bot.start();
  console.log(`Bot address: ${address}\n`);
  console.log("Commands: /send /image /audio /file /history /address /quit\n");

  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  rl.setPrompt("> ");
  rl.prompt();

  rl.on("line", async (line: string) => {
    const trimmed = line.trim();
    if (!trimmed) { rl.prompt(); return; }
    const parts = trimmed.split(/\s+/);
    const cmd = parts[0];
    try {
      switch (cmd) {
        case "/send": {
          const [, addr, ...rest] = parts;
          if (!addr || rest.length === 0) { console.log("Usage: /send <address> <message>"); break; }
          console.log(`Sent: ${bot.sendText(addr, rest.join(" "))}`);
          break;
        }
        case "/image": {
          if (!parts[1] || !parts[2]) { console.log("Usage: /image <address> <path>"); break; }
          console.log(`Sent image: ${await bot.sendImage(parts[1], parts[2])}`);
          break;
        }
        case "/audio": {
          if (!parts[1] || !parts[2]) { console.log("Usage: /audio <address> <path>"); break; }
          console.log(`Sent audio: ${await bot.sendAudio(parts[1], parts[2])}`);
          break;
        }
        case "/file": {
          if (!parts[1] || !parts[2]) { console.log("Usage: /file <address> <path>"); break; }
          console.log(`Sent file: ${await bot.sendFile(parts[1], parts[2])}`);
          break;
        }
        case "/history": {
          if (!parts[1]) { console.log("Usage: /history <address>"); break; }
          const msgs = bot.getMessages(parts[1]);
          if (msgs.length === 0) { console.log("No messages."); }
          else {
            for (const m of msgs) {
              const dir = m.isOutbound ? "→" : "←";
              const t = new Date(m.createdAt).toISOString().substring(11, 19);
              const p = m.content.length > 80 ? m.content.substring(0, 80) + "..." : m.content;
              console.log(`  ${dir} [${t}] (${m.contentType}) ${p}`);
            }
          }
          break;
        }
        case "/address":
          console.log(bot.address);
          break;
        case "/quit":
        case "/exit":
          await shutdown();
          break;
        default:
          console.log(`Unknown command: ${cmd}`);
      }
    } catch (err) {
      console.error("Error:", err instanceof Error ? err.message : err);
    }
    rl.prompt();
  });

  rl.on("close", shutdown);
}

// ── Main: parse args and dispatch ──────────────────────────

async function main() {
  const args = process.argv.slice(2);

  // Extract global options first
  let dataDir = DATA_DIR;
  let explicitSeed: string | undefined;
  const positional: string[] = [];

  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case "--data-dir":
        dataDir = args[++i];
        break;
      case "--seed":
        explicitSeed = args[++i];
        break;
      default:
        positional.push(args[i]);
    }
  }

  const subcommand = positional[0] ?? "help";

  // Resolve seed for commands that need it
  const resolveSeed = (): string => {
    if (explicitSeed) return explicitSeed;
    return loadOrCreateIdentity(dataDir).seed;
  };

  switch (subcommand) {
    case "help":
    case "--help":
    case "-h":
      cmdHelp();
      break;

    case "init":
      cmdInit(dataDir);
      break;

    case "address":
      cmdAddress(dataDir, explicitSeed);
      break;

    case "contacts": {
      const action = positional[1];
      const db = new MessageDb(dataDir);
      switch (action) {
        case "add": {
          const addr = positional[2];
          const alias = positional[3];
          if (!addr || !alias) {
            console.error("Usage: dchat contacts add <address> <alias>");
            process.exit(1);
          }
          if (!/^[0-9a-f]{64}$/i.test(addr)) {
            console.error("Address must be a 64-character hex string.");
            process.exit(1);
          }
          db.addContact(addr, alias);
          console.log(`Added contact: ${alias} → ${addr}`);
          break;
        }
        case "remove": {
          const target = positional[2];
          if (!target) {
            console.error("Usage: dchat contacts remove <alias|address>");
            process.exit(1);
          }
          if (db.removeContact(target)) {
            console.log(`Removed contact: ${target}`);
          } else {
            console.error(`Contact not found: ${target}`);
          }
          break;
        }
        case "list": {
          const contacts = db.listContacts();
          if (contacts.length === 0) {
            console.log("No contacts.");
          } else {
            for (const c of contacts) {
              const alias = c.name ? `${c.name} ` : "";
              console.log(`  ${alias}${c.address}`);
            }
          }
          break;
        }
        default:
          console.error("Usage: dchat contacts <add|remove|list>");
          process.exit(1);
      }
      db.close();
      break;
    }

    case "send": {
      const target = positional[1];
      const message = positional.slice(2).join(" ");
      if (!target || !message) {
        console.error("Usage: dchat send <address|alias> <message>");
        process.exit(1);
      }
      await cmdSend(dataDir, resolveSeed(), resolveAddress(dataDir, target), message);
      break;
    }

    case "send-image": {
      if (!positional[1] || !positional[2]) {
        console.error("Usage: dchat send-image <address|alias> <path>");
        process.exit(1);
      }
      await cmdSendMedia(dataDir, resolveSeed(), "image", resolveAddress(dataDir, positional[1]), positional[2]);
      break;
    }

    case "send-audio": {
      if (!positional[1] || !positional[2]) {
        console.error("Usage: dchat send-audio <address|alias> <path>");
        process.exit(1);
      }
      await cmdSendMedia(dataDir, resolveSeed(), "audio", resolveAddress(dataDir, positional[1]), positional[2]);
      break;
    }

    case "send-file": {
      if (!positional[1] || !positional[2]) {
        console.error("Usage: dchat send-file <address|alias> <path>");
        process.exit(1);
      }
      await cmdSendMedia(dataDir, resolveSeed(), "file", resolveAddress(dataDir, positional[1]), positional[2]);
      break;
    }

    case "history": {
      if (!positional[1]) {
        console.error("Usage: dchat history <address|alias> [limit]");
        process.exit(1);
      }
      const limit = positional[2] ? parseInt(positional[2], 10) : 50;
      cmdHistory(dataDir, resolveSeed(), resolveAddress(dataDir, positional[1]), limit);
      break;
    }

    case "listen":
      await cmdListen(dataDir, resolveSeed());
      break;

    case "interactive":
      await cmdInteractive(dataDir, resolveSeed());
      break;

    default:
      console.error(`Unknown command: ${subcommand}\nRun 'dchat help' for usage.`);
      process.exit(1);
  }
}

main().catch((err) => {
  console.error("Fatal:", err);
  process.exit(1);
});


path: src/client.ts
content: import crypto from "crypto";
import { EventEmitter } from "events";
import nkn from "nkn-sdk";
import type { ClientStatus } from "./types.js";

const NKN_SEED_RPC_SERVERS = [
  "http://seed.nkn.org:30003",
  "http://mainnet-seed-0001.nkn.org:30003",
  "http://mainnet-seed-0002.nkn.org:30003",
  "http://mainnet-seed-0003.nkn.org:30003",
];

export class NknClient extends EventEmitter {
  private client: nkn.MultiClient | null = null;
  private status: ClientStatus = { state: "disconnected" };
  private numSubClients: number;

  constructor(numSubClients = 4) {
    super();
    this.numSubClients = numSubClients;
  }

  getStatus(): ClientStatus {
    return { ...this.status };
  }

  getAddress(): string | undefined {
    return this.status.address;
  }

  /**
   * Generate a new random NKN seed (hex-encoded Ed25519 private key).
   */
  static generateSeed(): string {
    return crypto.randomBytes(32).toString("hex");
  }

  /**
   * Derive the NKN address from a seed without connecting to the network.
   */
  static deriveAddress(seed: string): string {
    const kp = nkn.crypto.keyPair(seed);
    return Buffer.from(kp.publicKey).toString("hex");
  }

  async connect(seed: string): Promise<ClientStatus> {
    if (this.client) await this.disconnect();
    this.updateStatus({ state: "connecting" });

    try {
      this.client = new nkn.MultiClient({
        seed,
        numSubClients: this.numSubClients,
        originalClient: false,
        rpcServerAddr: NKN_SEED_RPC_SERVERS[0],
      });

      await new Promise<void>((resolve, reject) => {
        const timeout = setTimeout(() => reject(new Error("Connection timeout after 30s")), 30000);
        this.client!.onConnect(() => {
          clearTimeout(timeout);
          resolve();
        });
      });

      this.updateStatus({
        state: "connected",
        address: this.client.addr,
        publicKey: this.client.getPublicKey(),
      });

      this.client.onMessage(({ src, payload }: { src: string; payload: Uint8Array | string }) => {
        const data = payload instanceof Uint8Array ? new TextDecoder().decode(payload) : payload;
        this.emit("message", src, data);
      });

      return this.getStatus();
    } catch (err) {
      this.updateStatus({ state: "disconnected" });
      this.client = null;
      throw err;
    }
  }

  async disconnect(): Promise<void> {
    if (this.client) {
      try {
        this.client.close();
      } catch {
        // ignore
      }
      this.client = null;
    }
    this.updateStatus({ state: "disconnected" });
  }

  async send(dest: string, data: string): Promise<void> {
    this.assertConnected();
    await this.client!.send(dest, data, { msgHoldingSeconds: 3600 });
  }

  sendNoReply(dest: string, data: string): void {
    this.assertConnected();
    // NKN SDK send() returns a promise even with noReply — catch to prevent
    // unhandled rejection when client disconnects before delivery completes.
    const p = this.client!.send(dest, data, { noReply: true, msgHoldingSeconds: 3600 });
    if (p && typeof p.catch === "function") {
      p.catch((err: unknown) => {
        console.error("[nkn] send failed (non-blocking):", err instanceof Error ? err.message : err);
      });
    }
  }

  private assertConnected(): void {
    if (!this.client || this.status.state !== "connected") {
      throw new Error("NKN client not connected");
    }
  }

  private updateStatus(status: ClientStatus): void {
    this.status = status;
    this.emit("statusChange", status);
  }
}


path: src/crypto.ts
content: import crypto from "crypto";

const ALGORITHM = "aes-128-gcm";
const KEY_BYTES = 16;
const NONCE_BYTES = 12;
const AUTH_TAG_BYTES = 16;

export interface EncryptResult {
  ciphertext: Buffer; // [nonce 12B] [encrypted data] [auth tag 16B]
  key: Buffer;
  nonce: Buffer;
}

/**
 * Encrypt with AES-128-GCM, nonce prepended (nMobile convention).
 * Output: [nonce (12 bytes)] [encrypted data] [auth tag (16 bytes)]
 */
export function encrypt(plaintext: Buffer): EncryptResult {
  const key = crypto.randomBytes(KEY_BYTES);
  const nonce = crypto.randomBytes(NONCE_BYTES);
  const cipher = crypto.createCipheriv(ALGORITHM, key, nonce);
  const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  const authTag = cipher.getAuthTag();
  const ciphertext = Buffer.concat([nonce, encrypted, authTag]);
  return { ciphertext, key, nonce };
}

/**
 * Decrypt AES-128-GCM with nonce prepended (nMobile convention).
 * Input: [nonce (12 bytes)] [encrypted data] [auth tag (16 bytes)]
 */
export function decrypt(data: Buffer, key: Buffer, nonceSize = NONCE_BYTES): Buffer {
  const nonce = data.subarray(0, nonceSize);
  const ciphertextWithTag = data.subarray(nonceSize);
  const encrypted = ciphertextWithTag.subarray(0, ciphertextWithTag.length - AUTH_TAG_BYTES);
  const authTag = ciphertextWithTag.subarray(ciphertextWithTag.length - AUTH_TAG_BYTES);
  const decipher = crypto.createDecipheriv(ALGORITHM, key, nonce);
  decipher.setAuthTag(authTag);
  return Buffer.concat([decipher.update(encrypted), decipher.final()]);
}


path: src/db.ts
content: import Database from "better-sqlite3";
import path from "path";
import fs from "fs";
import type { Message, MessageStatus } from "./types.js";

const SCHEMA = `
CREATE TABLE IF NOT EXISTS message (
  id TEXT PRIMARY KEY,
  sender TEXT NOT NULL,
  receiver TEXT NOT NULL,
  content_type TEXT NOT NULL,
  content TEXT NOT NULL DEFAULT '',
  status TEXT NOT NULL DEFAULT 'sending',
  is_outbound INTEGER NOT NULL DEFAULT 0,
  options TEXT,
  local_file_path TEXT,
  delete_at INTEGER,
  is_delete INTEGER NOT NULL DEFAULT 0,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_message_sender ON message(sender);
CREATE INDEX IF NOT EXISTS idx_message_receiver ON message(receiver);
CREATE INDEX IF NOT EXISTS idx_message_created ON message(created_at);
CREATE INDEX IF NOT EXISTS idx_message_delete_at ON message(delete_at) WHERE delete_at IS NOT NULL;

CREATE TABLE IF NOT EXISTS peer (
  address TEXT PRIMARY KEY,
  name TEXT,
  last_seen_at INTEGER,
  created_at INTEGER NOT NULL
);
`;

export class MessageDb {
  private db: Database.Database;

  constructor(dataDir: string) {
    fs.mkdirSync(dataDir, { recursive: true });
    const dbPath = path.join(dataDir, "messages.db");
    this.db = new Database(dbPath);
    this.db.pragma("journal_mode = WAL");
    this.db.pragma("foreign_keys = ON");
    this.db.exec(SCHEMA);
  }

  insertMessage(msg: Message): void {
    this.db
      .prepare(
        `INSERT OR IGNORE INTO message
         (id, sender, receiver, content_type, content, status, is_outbound, options, local_file_path, delete_at, created_at, updated_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      )
      .run(
        msg.id,
        msg.sender,
        msg.receiver,
        msg.contentType,
        msg.content,
        msg.status,
        msg.isOutbound ? 1 : 0,
        msg.options ?? null,
        msg.localFilePath ?? null,
        msg.deleteAt ?? null,
        msg.createdAt,
        msg.createdAt,
      );
  }

  updateStatus(id: string, status: MessageStatus): void {
    this.db.prepare("UPDATE message SET status = ?, updated_at = ? WHERE id = ?").run(status, Date.now(), id);
  }

  /** Update status only if current status is not already "delivered" or "read" (prevents downgrade). */
  updateStatusIfNotDelivered(id: string, status: MessageStatus): void {
    this.db
      .prepare("UPDATE message SET status = ?, updated_at = ? WHERE id = ? AND status NOT IN ('delivered', 'read')")
      .run(status, Date.now(), id);
  }

  updateLocalFilePath(id: string, filePath: string): void {
    this.db.prepare("UPDATE message SET local_file_path = ?, updated_at = ? WHERE id = ?").run(filePath, Date.now(), id);
  }

  hasMessage(id: string): boolean {
    const row = this.db.prepare("SELECT 1 FROM message WHERE id = ?").get(id);
    return !!row;
  }

  getMessages(peerAddress: string, limit = 50, before?: number): Message[] {
    const query = before
      ? `SELECT * FROM message WHERE ((sender = ? OR receiver = ?) AND is_delete = 0 AND created_at < ?) ORDER BY created_at DESC LIMIT ?`
      : `SELECT * FROM message WHERE ((sender = ? OR receiver = ?) AND is_delete = 0) ORDER BY created_at DESC LIMIT ?`;

    const rows = before
      ? (this.db.prepare(query).all(peerAddress, peerAddress, before, limit) as any[])
      : (this.db.prepare(query).all(peerAddress, peerAddress, limit) as any[]);

    return rows.map(this.rowToMessage).reverse();
  }

  /** Find and mark expired burn-after-read messages */
  burnExpired(): string[] {
    const now = Date.now();
    const expired = this.db
      .prepare("SELECT id FROM message WHERE delete_at IS NOT NULL AND delete_at <= ? AND is_delete = 0")
      .all(now) as { id: string }[];

    if (expired.length > 0) {
      const ids = expired.map((r) => r.id);
      this.db.prepare(`UPDATE message SET is_delete = 1, updated_at = ? WHERE id IN (${ids.map(() => "?").join(",")})`)
        .run(now, ...ids);
      return ids;
    }
    return [];
  }

  upsertPeer(address: string, name?: string): void {
    this.db
      .prepare(
        `INSERT INTO peer (address, name, last_seen_at, created_at)
         VALUES (?, ?, ?, ?)
         ON CONFLICT(address) DO UPDATE SET last_seen_at = ?, name = COALESCE(?, name)`,
      )
      .run(address, name ?? null, Date.now(), Date.now(), Date.now(), name ?? null);
  }

  // ── Contacts ─────────────────────────────────────────────

  addContact(address: string, alias: string): void {
    this.db
      .prepare(
        `INSERT INTO peer (address, name, last_seen_at, created_at)
         VALUES (?, ?, ?, ?)
         ON CONFLICT(address) DO UPDATE SET name = ?`,
      )
      .run(address, alias, Date.now(), Date.now(), alias);
  }

  removeContact(addressOrAlias: string): boolean {
    const address = this.resolveAlias(addressOrAlias) ?? addressOrAlias;
    const result = this.db.prepare("DELETE FROM peer WHERE address = ?").run(address);
    return result.changes > 0;
  }

  listContacts(): { address: string; name: string | null; lastSeenAt: number | null }[] {
    const rows = this.db
      .prepare("SELECT address, name, last_seen_at FROM peer ORDER BY name, address")
      .all() as any[];
    return rows.map((r) => ({ address: r.address, name: r.name, lastSeenAt: r.last_seen_at }));
  }

  /** Resolve an alias to an NKN address. Returns null if not found. */
  resolveAlias(alias: string): string | null {
    const row = this.db
      .prepare("SELECT address FROM peer WHERE name = ? COLLATE NOCASE")
      .get(alias) as { address: string } | undefined;
    return row?.address ?? null;
  }

  /** Get the alias for an NKN address. Returns null if no alias set. */
  getAlias(address: string): string | null {
    const row = this.db
      .prepare("SELECT name FROM peer WHERE address = ?")
      .get(address) as { name: string | null } | undefined;
    return row?.name ?? null;
  }

  close(): void {
    this.db.close();
  }

  private rowToMessage(row: any): Message {
    return {
      id: row.id,
      sender: row.sender,
      receiver: row.receiver,
      contentType: row.content_type,
      content: row.content,
      status: row.status,
      isOutbound: !!row.is_outbound,
      options: row.options ?? undefined,
      localFilePath: row.local_file_path ?? undefined,
      deleteAt: row.delete_at ?? undefined,
      createdAt: row.created_at,
    };
  }
}


path: src/index.ts
content: export { DchatBot } from "./bot.js";
export { NknClient } from "./client.js";
export { IpfsService } from "./ipfs.js";
export { MediaService } from "./media.js";
export { MessageDb } from "./db.js";
export { SafeStorage } from "./storage.js";
export type { StoredIdentity } from "./storage.js";
export { encrypt, decrypt } from "./crypto.js";
export type {
  BotConfig,
  ClientStatus,
  IncomingMessage,
  IpfsGateway,
  Message,
  MessageContentType,
  MessageData,
  MessageOptions,
  MessageStatus,
} from "./types.js";


path: src/ipfs.ts
content: import dns from "dns/promises";
import { isIP } from "net";
import FormData from "form-data";
import https from "https";
import http from "http";
import type { IpfsGateway } from "./types.js";

/** Public IPFS gateways — well-known, community-operated. */
const DEFAULT_GATEWAYS: IpfsGateway[] = [
  { host: "ipfs.io", port: 443, protocol: "https:" },
  { host: "dweb.link", port: 443, protocol: "https:" },
];

export class IpfsService {
  private gateways: IpfsGateway[];

  constructor(gateways?: IpfsGateway[]) {
    this.gateways = gateways?.length ? gateways : DEFAULT_GATEWAYS;
  }

  getPrimaryIp(): string {
    return this.gateways[0]?.host ?? "ipfs.io";
  }

  async upload(data: Buffer, fileName: string): Promise<string> {
    let lastError: Error | null = null;
    for (const gw of this.gateways) {
      try {
        return await this.uploadToGateway(gw, data, fileName);
      } catch (err) {
        lastError = err instanceof Error ? err : new Error(String(err));
      }
    }
    throw lastError ?? new Error("No IPFS gateways configured");
  }

  async download(ipfsHash: string, preferredIp?: string): Promise<Buffer> {
    const ordered = await this.orderGateways(preferredIp);
    let lastError: Error | null = null;
    for (const gw of ordered) {
      try {
        return await this.downloadFromGateway(gw, ipfsHash);
      } catch (err) {
        lastError = err instanceof Error ? err : new Error(String(err));
      }
    }
    throw lastError ?? new Error("No IPFS gateways configured");
  }

  /**
   * Check if an IP address is private/reserved (RFC 1918, loopback, link-local, etc.).
   * Handles both IPv4 and IPv6.
   */
  private isPrivateOrReservedIp(ip: string): boolean {
    // IPv4
    const v4Parts = ip.split(".");
    if (v4Parts.length === 4) {
      const octets = v4Parts.map(Number);
      if (octets.some((o) => isNaN(o) || o < 0 || o > 255)) return true;
      const [a, b] = octets;
      if (a === 10 || a === 127 || a === 0) return true;         // 10.0.0.0/8, loopback, 0.0.0.0
      if (a === 172 && b >= 16 && b <= 31) return true;           // 172.16.0.0/12
      if (a === 192 && b === 168) return true;                    // 192.168.0.0/16
      if (a === 169 && b === 254) return true;                    // 169.254.0.0/16 link-local
      return false;
    }
    // IPv6
    const normalized = ip.toLowerCase();
    if (normalized === "::1") return true;                         // loopback
    if (normalized.startsWith("fe80:")) return true;               // link-local
    if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true; // ULA
    if (normalized === "::" || normalized === "::0") return true;  // unspecified
    // IPv4-mapped IPv6 (::ffff:10.0.0.1)
    const v4Mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
    if (v4Mapped) return this.isPrivateOrReservedIp(v4Mapped[1]);
    return false;
  }

  /**
   * Resolve a hostname to IP addresses and check that none are private/reserved.
   * Returns true if the host is safe to connect to (all resolved IPs are public).
   */
  private async isPublicHost(host: string): Promise<boolean> {
    // If it's already an IP literal, check directly
    if (isIP(host)) {
      return !this.isPrivateOrReservedIp(host);
    }
    // Resolve DNS and check ALL resolved addresses
    try {
      const addresses = await dns.resolve(host);
      if (addresses.length === 0) return false;
      return addresses.every((addr) => !this.isPrivateOrReservedIp(addr));
    } catch {
      return false; // DNS resolution failed — reject
    }
  }

  private async orderGateways(preferredIp?: string): Promise<IpfsGateway[]> {
    if (!preferredIp) return this.gateways;
    const preferred = this.gateways.filter((g) => g.host === preferredIp);
    const rest = this.gateways.filter((g) => g.host !== preferredIp);
    if (preferred.length === 0) {
      // Ad-hoc gateway from message options — resolve DNS and verify public
      const isPublic = await this.isPublicHost(preferredIp);
      if (!isPublic) {
        console.warn(`[IpfsService] Rejected non-public gateway: ${preferredIp}`);
        return this.gateways;
      }
      return [{ host: preferredIp, port: 80, protocol: "http:" }, ...this.gateways];
    }
    return [...preferred, ...rest];
  }

  private uploadToGateway(gw: IpfsGateway, data: Buffer, fileName: string): Promise<string> {
    const form = new FormData();
    form.append("file", data, { filename: fileName });
    const headers: Record<string, string> = { ...form.getHeaders() };
    if (gw.authHeader) headers["Authorization"] = gw.authHeader;

    return new Promise<string>((resolve, reject) => {
      const client = gw.protocol === "https:" ? https : http;
      const req = client.request(
        { hostname: gw.host, port: gw.port, path: "/api/v0/add", method: "POST", headers },
        (res) => {
          let body = "";
          res.on("data", (chunk: Buffer) => (body += chunk.toString()));
          res.on("end", () => {
            const headerHash = res.headers["ipfs-hash"];
            if (headerHash) {
              resolve(Array.isArray(headerHash) ? headerHash[0] : headerHash);
              return;
            }
            if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
              try {
                const json = JSON.parse(body);
                if (json.Hash) resolve(json.Hash as string);
                else reject(new Error(`No Hash in IPFS response: ${body}`));
              } catch {
                reject(new Error(`Failed to parse IPFS response: ${body}`));
              }
            } else {
              reject(new Error(`IPFS upload failed (${res.statusCode}): ${body}`));
            }
          });
        },
      );
      req.on("error", reject);
      req.setTimeout(30000, () => req.destroy(new Error("IPFS upload timeout")));
      form.pipe(req);
    });
  }

  private downloadFromGateway(gw: IpfsGateway, ipfsHash: string): Promise<Buffer> {
    const headers: Record<string, string> = {};
    if (gw.authHeader) headers["Authorization"] = gw.authHeader;

    return new Promise<Buffer>((resolve, reject) => {
      const client = gw.protocol === "https:" ? https : http;
      const req = client.request(
        {
          hostname: gw.host,
          port: gw.port,
          path: `/api/v0/cat?arg=${encodeURIComponent(ipfsHash)}`,
          method: "POST",
          headers,
        },
        (res) => {
          if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
            res.resume();
            reject(new Error(`IPFS download failed (${res.statusCode}) for ${ipfsHash}`));
            return;
          }
          const chunks: Buffer[] = [];
          res.on("data", (chunk: Buffer) => chunks.push(chunk));
          res.on("end", () => resolve(Buffer.concat(chunks)));
        },
      );
      req.on("error", reject);
      req.setTimeout(60000, () => req.destroy(new Error("IPFS download timeout")));
      req.end();
    });
  }
}


path: src/media.ts
content: import fs from "fs";
import path from "path";
import { encrypt, decrypt } from "./crypto.js";
import { IpfsService } from "./ipfs.js";
import type { MessageOptions } from "./types.js";

const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB

export interface MediaResult {
  content: string; // IPFS hash or inline base64
  options: MessageOptions;
  localFilePath: string;
}

export class MediaService {
  private cacheDir: string;
  private ipfs: IpfsService;

  constructor(ipfs: IpfsService, dataDir: string) {
    this.ipfs = ipfs;
    this.cacheDir = path.join(dataDir, "media-cache");
    fs.mkdirSync(this.cacheDir, { recursive: true });
  }

  /**
   * Process and upload an image file.
   * Bot-optimized: no thumbnail generation (no sharp dependency), sends full image only.
   * Still nMobile-compatible wire format.
   */
  async uploadImage(filePath: string): Promise<MediaResult> {
    const imageBuffer = fs.readFileSync(filePath);
    if (imageBuffer.length > MAX_FILE_SIZE) {
      throw new Error(`File too large: ${imageBuffer.length} bytes (max ${MAX_FILE_SIZE})`);
    }

    const ext = path.extname(filePath).replace(/^\./, "") || "jpg";
    const { ciphertext, key } = encrypt(imageBuffer);
    const ipfsHash = await this.ipfs.upload(ciphertext, "image.enc");
    const primaryIp = this.ipfs.getPrimaryIp();

    const localFilePath = path.join(this.cacheDir, `${ipfsHash}.${ext}`);
    fs.writeFileSync(localFilePath, imageBuffer);

    const options: MessageOptions = {
      ipfsHash,
      ipfsIp: primaryIp,
      ipfsEncrypt: 1,
      ipfsEncryptAlgorithm: "AES/GCM/NoPadding",
      ipfsEncryptKeyBytes: Array.from(key),
      ipfsEncryptNonceSize: 12,
      fileType: 1,
      fileExt: ext,
      fileMimeType: `image/${ext === "jpg" ? "jpeg" : ext}`,
      fileSize: imageBuffer.length,
    };

    return { content: ipfsHash, options, localFilePath };
  }

  /**
   * Process and upload an audio file.
   * Sends as inline base64 in nMobile data-URI format for small files,
   * or via IPFS for larger ones.
   */
  async uploadAudio(filePath: string, durationSeconds?: number): Promise<MediaResult> {
    const audioBuffer = fs.readFileSync(filePath);
    if (audioBuffer.length > MAX_FILE_SIZE) {
      throw new Error(`File too large: ${audioBuffer.length} bytes (max ${MAX_FILE_SIZE})`);
    }

    const ext = path.extname(filePath).replace(/^\./, "") || "aac";
    const localFilePath = path.join(this.cacheDir, `${Date.now()}.${ext}`);
    fs.writeFileSync(localFilePath, audioBuffer);

    const options: MessageOptions = {
      fileType: 2,
      fileExt: ext,
      fileMimeType: `audio/${ext}`,
      mediaDuration: durationSeconds,
    };

    // Inline for small audio (<256KB), IPFS for larger
    if (audioBuffer.length < 256 * 1024) {
      const content = `![audio](data:audio/x-${ext};base64,${audioBuffer.toString("base64")})`;
      return { content, options, localFilePath };
    }

    // Large audio: encrypt and upload to IPFS
    const { ciphertext, key } = encrypt(audioBuffer);
    const ipfsHash = await this.ipfs.upload(ciphertext, `audio.${ext}.enc`);
    options.ipfsHash = ipfsHash;
    options.ipfsIp = this.ipfs.getPrimaryIp();
    options.ipfsEncrypt = 1;
    options.ipfsEncryptAlgorithm = "AES/GCM/NoPadding";
    options.ipfsEncryptKeyBytes = Array.from(key);
    options.ipfsEncryptNonceSize = 12;
    options.fileSize = audioBuffer.length;

    return { content: ipfsHash, options, localFilePath };
  }

  /**
   * Process and upload a generic file.
   */
  async uploadFile(filePath: string): Promise<MediaResult> {
    const fileBuffer = fs.readFileSync(filePath);
    if (fileBuffer.length > MAX_FILE_SIZE) {
      throw new Error(`File too large: ${fileBuffer.length} bytes (max ${MAX_FILE_SIZE})`);
    }

    const fileName = path.basename(filePath);
    const ext = path.extname(filePath).replace(/^\./, "") || "bin";
    const { ciphertext, key } = encrypt(fileBuffer);
    const ipfsHash = await this.ipfs.upload(ciphertext, `${fileName}.enc`);
    const primaryIp = this.ipfs.getPrimaryIp();

    const localFilePath = path.join(this.cacheDir, `${ipfsHash}.${ext}`);
    fs.writeFileSync(localFilePath, fileBuffer);

    const options: MessageOptions = {
      fileType: 0,
      fileName,
      fileExt: ext,
      fileSize: fileBuffer.length,
      ipfsHash,
      ipfsIp: primaryIp,
      ipfsEncrypt: 1,
      ipfsEncryptAlgorithm: "AES/GCM/NoPadding",
      ipfsEncryptKeyBytes: Array.from(key),
      ipfsEncryptNonceSize: 12,
    };

    return { content: ipfsHash, options, localFilePath };
  }

  /**
   * Download and decrypt media from IPFS.
   * Returns local file path.
   */
  async downloadMedia(
    ipfsHash: string,
    keyBytes: number[],
    nonceSize = 12,
    fileExt = "bin",
    preferredIp?: string,
  ): Promise<string> {
    const ext = fileExt.startsWith(".") ? fileExt : `.${fileExt}`;
    const cachedPath = path.join(this.cacheDir, `${ipfsHash}${ext}`);
    if (fs.existsSync(cachedPath)) return cachedPath;

    const encryptedData = await this.ipfs.download(ipfsHash, preferredIp);
    const key = Buffer.from(keyBytes);
    const decrypted = decrypt(encryptedData, key, nonceSize);
    fs.writeFileSync(cachedPath, decrypted);
    return cachedPath;
  }

  /**
   * Save inline audio (nMobile base64 data-URI format) to local cache.
   */
  saveInlineAudio(messageId: string, content: string, fileExt = "aac"): string {
    const ext = fileExt.startsWith(".") ? fileExt : `.${fileExt}`;
    const localPath = path.join(this.cacheDir, `${messageId}${ext}`);
    const match = content.match(/^!\[audio\]\(data:[^;]+;base64,(.+)\)$/s);
    const rawBase64 = match ? match[1] : content;
    fs.writeFileSync(localPath, Buffer.from(rawBase64, "base64"));
    return localPath;
  }

  getCacheDir(): string {
    return this.cacheDir;
  }
}


path: src/storage.ts
content: import crypto from "crypto";
import fs from "fs";
import path from "path";

const ALGORITHM = "aes-256-gcm";
const KEY_BYTES = 32;
const SALT_BYTES = 16;
const NONCE_BYTES = 12;
const AUTH_TAG_BYTES = 16;
const PBKDF2_ITERATIONS = 100_000;

/**
 * Safe storage for bot identity (seed + address).
 * Encrypts data at rest using AES-256-GCM with a PBKDF2-derived key.
 *
 * File format: [salt 16B] [nonce 12B] [ciphertext] [authTag 16B]
 * Plaintext is JSON: { seed, address }
 */
export interface StoredIdentity {
  seed: string;
  address: string;
}

export class SafeStorage {
  private filePath: string;

  constructor(dataDir: string, fileName = "identity.enc") {
    fs.mkdirSync(dataDir, { recursive: true });
    this.filePath = path.join(dataDir, fileName);
  }

  /** Save seed and address encrypted with passphrase. */
  save(identity: StoredIdentity, passphrase: string): void {
    const plaintext = Buffer.from(JSON.stringify(identity), "utf-8");
    const salt = crypto.randomBytes(SALT_BYTES);
    const key = crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_BYTES, "sha256");
    const nonce = crypto.randomBytes(NONCE_BYTES);

    const cipher = crypto.createCipheriv(ALGORITHM, key, nonce);
    const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
    const authTag = cipher.getAuthTag();

    // [salt][nonce][ciphertext][authTag]
    const output = Buffer.concat([salt, nonce, encrypted, authTag]);
    fs.writeFileSync(this.filePath, output, { mode: 0o600 });
  }

  /** Load and decrypt identity. Returns null if file doesn't exist. */
  load(passphrase: string): StoredIdentity | null {
    if (!fs.existsSync(this.filePath)) return null;

    const data = fs.readFileSync(this.filePath);
    const salt = data.subarray(0, SALT_BYTES);
    const nonce = data.subarray(SALT_BYTES, SALT_BYTES + NONCE_BYTES);
    const ciphertextWithTag = data.subarray(SALT_BYTES + NONCE_BYTES);
    const encrypted = ciphertextWithTag.subarray(0, ciphertextWithTag.length - AUTH_TAG_BYTES);
    const authTag = ciphertextWithTag.subarray(ciphertextWithTag.length - AUTH_TAG_BYTES);

    const key = crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_BYTES, "sha256");
    const decipher = crypto.createDecipheriv(ALGORITHM, key, nonce);
    decipher.setAuthTag(authTag);

    const plaintext = Buffer.concat([decipher.update(encrypted), decipher.final()]);
    return JSON.parse(plaintext.toString("utf-8")) as StoredIdentity;
  }

  /** Check if an identity file exists. */
  exists(): boolean {
    return fs.existsSync(this.filePath);
  }

  /** Delete the identity file. */
  delete(): void {
    if (fs.existsSync(this.filePath)) fs.unlinkSync(this.filePath);
  }

  getFilePath(): string {
    return this.filePath;
  }
}


path: src/types.ts
content: /** Content types compatible with dchat/nMobile protocol */
export type MessageContentType =
  | "text"
  | "textExtension"
  | "image"
  | "audio"
  | "file"
  | "receipt"
  | "read"
  | "contact"
  | "contactOptions";

export type MessageStatus = "sending" | "sent" | "delivered" | "read" | "failed";

/** Wire format: JSON sent over NKN between peers */
export interface MessageData {
  id: string;
  contentType: MessageContentType;
  content?: string;
  options?: MessageOptions;
  topic?: string;
  targetID?: string; // for receipts: original message ID
  readIds?: string[]; // for read receipts
  timestamp: number;
}

/** nMobile-compatible message options for media/encryption metadata */
export interface MessageOptions {
  deleteAfterSeconds?: number;
  updateBurnAfterAt?: number;

  // IPFS media (nMobile format)
  ipfsHash?: string;
  ipfsIp?: string;
  ipfsEncrypt?: number; // 1 = encrypted
  ipfsEncryptAlgorithm?: string; // "AES/GCM/NoPadding"
  ipfsEncryptKeyBytes?: number[]; // AES key as byte array
  ipfsEncryptNonceSize?: number; // 12

  // Thumbnail (images only)
  ipfsThumbnailHash?: string;
  ipfsThumbnailIp?: string;
  ipfsThumbnailEncrypt?: number;
  ipfsThumbnailEncryptAlgorithm?: string;
  ipfsThumbnailEncryptKeyBytes?: number[];
  ipfsThumbnailEncryptNonceSize?: number;

  // File metadata
  fileType?: number; // 0=file, 1=image, 2=audio
  fileName?: string;
  fileExt?: string;
  fileMimeType?: string;
  fileSize?: number;
  mediaWidth?: number;
  mediaHeight?: number;
  mediaDuration?: number; // seconds
}

/** Stored message record */
export interface Message {
  id: string;
  sender: string;
  receiver: string;
  contentType: MessageContentType;
  content: string;
  status: MessageStatus;
  isOutbound: boolean;
  options?: string; // JSON-serialized MessageOptions
  localFilePath?: string;
  deleteAt?: number;
  createdAt: number;
}

/** NKN client connection state */
export interface ClientStatus {
  state: "disconnected" | "connecting" | "connected";
  address?: string;
  publicKey?: string;
}

/** Incoming message event payload */
export interface IncomingMessage {
  id: string;
  from: string;
  contentType: MessageContentType;
  content: string;
  options?: MessageOptions;
  timestamp: number;
  /** Local path to downloaded media file (populated after auto-download) */
  localFilePath?: string;
}

/** IPFS gateway configuration */
export interface IpfsGateway {
  host: string;
  port: number;
  protocol: "http:" | "https:";
  authHeader?: string;
}

/** Bot configuration */
export interface BotConfig {
  /** NKN wallet seed (hex string). Generated if not provided. */
  seed?: string;
  /** Data directory for cache and database. Defaults to ~/.dchat-clawhub */
  dataDir?: string;
  /** IPFS gateways. Defaults to nMobile gateway. */
  ipfsGateways?: IpfsGateway[];
  /** Number of NKN sub-clients. Default: 4 */
  numSubClients?: number;
  /** Auto-download received media files. Default: true */
  autoDownloadMedia?: boolean;
}


path: tests/crypto.test.ts
content: import { describe, it, expect } from "vitest";
import { encrypt, decrypt } from "../src/crypto.js";

describe("AES-128-GCM crypto", () => {
  it("encrypts and decrypts a buffer", () => {
    const plaintext = Buffer.from("hello decentralized world");
    const { ciphertext, key } = encrypt(plaintext);

    // Ciphertext should be nonce(12) + encrypted + authTag(16)
    expect(ciphertext.length).toBeGreaterThan(12 + 16);

    const decrypted = decrypt(ciphertext, key);
    expect(decrypted.toString()).toBe("hello decentralized world");
  });

  it("rejects tampered ciphertext", () => {
    const plaintext = Buffer.from("sensitive data");
    const { ciphertext, key } = encrypt(plaintext);

    // Tamper with a byte in the encrypted data
    ciphertext[15] ^= 0xff;

    expect(() => decrypt(ciphertext, key)).toThrow();
  });

  it("rejects wrong key", () => {
    const plaintext = Buffer.from("secret");
    const { ciphertext } = encrypt(plaintext);
    const wrongKey = Buffer.alloc(16, 0);

    expect(() => decrypt(ciphertext, wrongKey)).toThrow();
  });

  it("produces different ciphertext for same plaintext", () => {
    const plaintext = Buffer.from("determinism test");
    const r1 = encrypt(plaintext);
    const r2 = encrypt(plaintext);

    expect(r1.ciphertext).not.toEqual(r2.ciphertext);
    expect(r1.key).not.toEqual(r2.key);
  });

  it("handles empty buffer", () => {
    const plaintext = Buffer.alloc(0);
    const { ciphertext, key } = encrypt(plaintext);
    const decrypted = decrypt(ciphertext, key);
    expect(decrypted.length).toBe(0);
  });

  it("handles large buffer", () => {
    const plaintext = Buffer.alloc(1024 * 1024, 0xab); // 1MB
    const { ciphertext, key } = encrypt(plaintext);
    const decrypted = decrypt(ciphertext, key);
    expect(decrypted).toEqual(plaintext);
  });

  it("key bytes roundtrip through number array (nMobile format)", () => {
    const plaintext = Buffer.from("nMobile compatibility test");
    const { ciphertext, key } = encrypt(plaintext);

    // Simulate nMobile wire format: key as number[]
    const keyBytes: number[] = Array.from(key);
    const restoredKey = Buffer.from(keyBytes);

    const decrypted = decrypt(ciphertext, restoredKey);
    expect(decrypted.toString()).toBe("nMobile compatibility test");
  });
});


path: tests/db.test.ts
content: import { describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "fs";
import os from "os";
import path from "path";
import { MessageDb } from "../src/db.js";

describe("MessageDb", () => {
  let db: MessageDb;
  let tmpDir: string;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dchat-test-"));
    db = new MessageDb(tmpDir);
  });

  afterEach(() => {
    db.close();
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it("inserts and retrieves messages", () => {
    db.insertMessage({
      id: "msg-1",
      sender: "alice",
      receiver: "bob",
      contentType: "text",
      content: "hello",
      status: "sent",
      isOutbound: true,
      createdAt: 1000,
    });

    const msgs = db.getMessages("alice", 10);
    expect(msgs).toHaveLength(1);
    expect(msgs[0].content).toBe("hello");
    expect(msgs[0].isOutbound).toBe(true);
  });

  it("deduplicates by message id", () => {
    const msg = {
      id: "msg-dup",
      sender: "alice",
      receiver: "bob",
      contentType: "text" as const,
      content: "hello",
      status: "sent" as const,
      isOutbound: true,
      createdAt: 1000,
    };
    db.insertMessage(msg);
    db.insertMessage(msg); // duplicate
    expect(db.getMessages("alice", 10)).toHaveLength(1);
  });

  it("hasMessage works", () => {
    expect(db.hasMessage("nonexistent")).toBe(false);
    db.insertMessage({
      id: "exists",
      sender: "a",
      receiver: "b",
      contentType: "text",
      content: "",
      status: "sent",
      isOutbound: true,
      createdAt: 1000,
    });
    expect(db.hasMessage("exists")).toBe(true);
  });

  it("updates status", () => {
    db.insertMessage({
      id: "msg-status",
      sender: "a",
      receiver: "b",
      contentType: "text",
      content: "hi",
      status: "sending",
      isOutbound: true,
      createdAt: 1000,
    });
    db.updateStatus("msg-status", "delivered");
    const msgs = db.getMessages("a", 10);
    expect(msgs[0].status).toBe("delivered");
  });

  it("burns expired messages", () => {
    db.insertMessage({
      id: "burn-me",
      sender: "a",
      receiver: "b",
      contentType: "text",
      content: "secret",
      status: "delivered",
      isOutbound: false,
      deleteAt: Date.now() - 1000, // already expired
      createdAt: 1000,
    });

    const burned = db.burnExpired();
    expect(burned).toEqual(["burn-me"]);

    // Should not appear in messages
    const msgs = db.getMessages("a", 10);
    expect(msgs).toHaveLength(0);
  });

  it("upserts peers", () => {
    db.upsertPeer("alice.nkn", "Alice");
    db.upsertPeer("alice.nkn", "Alice Bot"); // update name
    // No error = success (we don't expose peer reads yet)
  });
});


path: tests/e2e-self-echo.ts
content: #!/usr/bin/env node
/**
 * End-to-end self-echo test:
 *   1. Create a new bot identity (wallet/seed)
 *   2. Save seed + NKN address in SafeStorage (encrypted)
 *   3. Connect to NKN network
 *   4. Send a text message to self (own NKN address)
 *   5. Verify the echo arrives back
 *   6. Verify message is persisted in DB
 *   7. Verify SafeStorage can reload the identity
 */

import fs from "fs";
import os from "os";
import path from "path";
import { DchatBot } from "../src/bot.js";
import { NknClient } from "../src/client.js";
import { SafeStorage } from "../src/storage.js";
import type { IncomingMessage } from "../src/types.js";

const TIMEOUT_MS = 120_000; // 2 min — NKN connection can be slow
const TEST_PASSPHRASE = "test-e2e-passphrase-2024";
const ECHO_TEXT = `echo-test-${Date.now()}`;

let tmpDir: string;
let bot: DchatBot | null = null;

function log(msg: string) {
  const ts = new Date().toISOString().substring(11, 23);
  console.log(`[${ts}] ${msg}`);
}

function fail(msg: string): never {
  console.error(`\n  FAIL: ${msg}\n`);
  cleanup();
  process.exit(1);
}

function pass(msg: string) {
  console.log(`  PASS: ${msg}`);
}

function cleanup() {
  if (bot) {
    bot.stop().catch(() => {});
    bot = null;
  }
  if (tmpDir && fs.existsSync(tmpDir)) {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  }
}

async function main() {
  console.log("\n=== dchat-clawhub E2E Self-Echo Test ===\n");

  // Set up temp directory
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dchat-e2e-"));
  log(`Data dir: ${tmpDir}`);

  // ── Step 1: Create identity ──────────────────────────
  log("Step 1: Creating new bot identity...");
  const seed = NknClient.generateSeed();
  if (!seed || seed.length !== 64) fail("Seed generation failed");
  pass(`Generated seed: ${seed.substring(0, 8)}...${seed.substring(56)}`);

  // ── Step 2: Save identity to SafeStorage ─────────────
  log("Step 2: Saving identity to SafeStorage...");
  const storage = new SafeStorage(tmpDir);

  // We'll save the address after connecting — for now save seed with placeholder
  // (address is derived from seed, but only known after NKN connection)
  pass(`SafeStorage file: ${storage.getFilePath()}`);

  // ── Step 3: Connect to NKN network ───────────────────
  log("Step 3: Connecting to NKN network...");
  bot = new DchatBot({ seed, dataDir: tmpDir });

  const address = await bot.start();
  if (!address) fail("No address returned from start()");
  pass(`Connected! NKN address: ${address}`);

  // Now save the full identity
  storage.save({ seed, address }, TEST_PASSPHRASE);
  pass(`Identity saved to encrypted storage`);

  // ── Step 4: Send echo message to self ────────────────
  log("Step 4: Sending echo message to self...");

  const echoReceived = new Promise<IncomingMessage>((resolve, reject) => {
    const timer = setTimeout(
      () => reject(new Error("Echo timeout — no message received within 60s")),
      60_000,
    );

    bot!.on("message", (msg: IncomingMessage) => {
      clearTimeout(timer);
      resolve(msg);
    });
  });

  const sentId = bot.sendText(address, ECHO_TEXT);
  pass(`Sent message ID: ${sentId}`);

  // ── Step 5: Wait for echo ────────────────────────────
  log("Step 5: Waiting for echo message...");
  const echoMsg = await echoReceived;

  if (echoMsg.content !== ECHO_TEXT) {
    fail(`Content mismatch: expected "${ECHO_TEXT}", got "${echoMsg.content}"`);
  }
  if (echoMsg.from !== address) {
    fail(`Sender mismatch: expected self (${address}), got ${echoMsg.from}`);
  }
  if (echoMsg.contentType !== "text") {
    fail(`ContentType mismatch: expected "text", got "${echoMsg.contentType}"`);
  }
  pass(`Echo received! Content: "${echoMsg.content}"`);
  pass(`From: ${echoMsg.from}`);

  // ── Step 6: Verify DB persistence ────────────────────
  log("Step 6: Verifying message in database...");
  const history = bot.getMessages(address, 10);
  if (history.length === 0) fail("No messages found in DB");

  const dbMsg = history.find((m) => m.id === sentId);
  if (!dbMsg) fail(`Sent message ${sentId} not found in DB`);
  if (dbMsg.content !== ECHO_TEXT) fail(`DB content mismatch`);
  if (dbMsg.status !== "delivered") {
    // May still be "sent" if receipt loop hasn't fired, but self-echo sets it to "delivered"
    log(`  Note: message status is "${dbMsg.status}" (expected "delivered")`);
  }
  pass(`Message persisted in DB: id=${dbMsg.id}, status=${dbMsg.status}`);

  // ── Step 7: Verify SafeStorage reload ────────────────
  log("Step 7: Verifying SafeStorage reload...");
  const loaded = storage.load(TEST_PASSPHRASE);
  if (!loaded) fail("Failed to load identity from SafeStorage");
  if (loaded.seed !== seed) fail("Seed mismatch after reload");
  if (loaded.address !== address) fail("Address mismatch after reload");
  pass(`Identity reloaded: seed matches, address matches`);

  // Verify wrong passphrase fails
  let wrongPassFailed = false;
  try {
    storage.load("wrong-passphrase");
  } catch {
    wrongPassFailed = true;
  }
  if (!wrongPassFailed) fail("SafeStorage should reject wrong passphrase");
  pass(`Wrong passphrase correctly rejected`);

  // ── Done ─────────────────────────────────────────────
  console.log("\n=== All E2E tests passed! ===\n");

  await bot.stop();
  bot = null;

  // Clean up
  fs.rmSync(tmpDir, { recursive: true, force: true });
  process.exit(0);
}

// Global timeout
const globalTimer = setTimeout(() => {
  fail(`Global timeout (${TIMEOUT_MS / 1000}s) exceeded`);
}, TIMEOUT_MS);

main()
  .catch((err) => {
    fail(`Unhandled error: ${err.message ?? err}`);
  })
  .finally(() => {
    clearTimeout(globalTimer);
  });


path: tests/storage.test.ts
content: import { describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "fs";
import os from "os";
import path from "path";
import { SafeStorage } from "../src/storage.js";

describe("SafeStorage", () => {
  let tmpDir: string;
  let storage: SafeStorage;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dchat-storage-test-"));
    storage = new SafeStorage(tmpDir);
  });

  afterEach(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it("saves and loads identity", () => {
    const identity = { seed: "abcd1234".repeat(8), address: "abc123.def456" };
    storage.save(identity, "my-passphrase");
    expect(storage.exists()).toBe(true);

    const loaded = storage.load("my-passphrase");
    expect(loaded).toEqual(identity);
  });

  it("returns null when file does not exist", () => {
    expect(storage.exists()).toBe(false);
    expect(storage.load("anything")).toBeNull();
  });

  it("rejects wrong passphrase", () => {
    storage.save({ seed: "seed", address: "addr" }, "correct");
    expect(() => storage.load("wrong")).toThrow();
  });

  it("encrypts data at rest (not plaintext)", () => {
    const identity = { seed: "supersecret", address: "myaddr" };
    storage.save(identity, "pass");

    const raw = fs.readFileSync(storage.getFilePath());
    const rawStr = raw.toString("utf-8");
    expect(rawStr).not.toContain("supersecret");
    expect(rawStr).not.toContain("myaddr");
  });

  it("sets restrictive file permissions", () => {
    storage.save({ seed: "s", address: "a" }, "p");
    const stat = fs.statSync(storage.getFilePath());
    const mode = stat.mode & 0o777;
    expect(mode).toBe(0o600);
  });

  it("deletes identity file", () => {
    storage.save({ seed: "s", address: "a" }, "p");
    expect(storage.exists()).toBe(true);
    storage.delete();
    expect(storage.exists()).toBe(false);
  });

  it("handles re-save (overwrite)", () => {
    storage.save({ seed: "old", address: "old-addr" }, "pass1");
    storage.save({ seed: "new", address: "new-addr" }, "pass2");

    expect(() => storage.load("pass1")).toThrow(); // old passphrase no longer works
    const loaded = storage.load("pass2");
    expect(loaded).toEqual({ seed: "new", address: "new-addr" });
  });
});


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}


path: vitest.config.ts
content: import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    testTimeout: 15000,
  },
});


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