diff --git a/.claude/rules/go.md b/.claude/rules/go.md new file mode 100644 index 000000000..5e089e738 --- /dev/null +++ b/.claude/rules/go.md @@ -0,0 +1,11 @@ +--- +paths: + - "**/*.go" +--- + +- Follow standard Go conventions: `gofmt`, exported names have doc comments. +- Error handling: always check and return errors, never ignore with `_`. +- Use the functional options pattern for configuration (see `sdk/go/config.go`). +- Shared packages live in `pkg/` — check there before creating new utilities. +- Test files go in the same package with `_test.go` suffix. +- Run tests from repo root: `go test ./sdk/go/...` (scoped) or `go test ./...` (all). diff --git a/.claude/rules/solidity.md b/.claude/rules/solidity.md new file mode 100644 index 000000000..a0031115e --- /dev/null +++ b/.claude/rules/solidity.md @@ -0,0 +1,12 @@ +--- +paths: + - "contracts/**/*.sol" +--- + +- Foundry project — use `forge build`, `forge test`, `forge fmt`. +- NatSpec comments on all public/external functions. +- Security first: validate all inputs, check for reentrancy, use OpenZeppelin where applicable. +- Test files in `contracts/test/` with `.t.sol` extension. +- Gas optimization matters — avoid unnecessary storage writes. +- Style guide: https://github.com/layer-3/clearsync/blob/master/contracts/solidity-style-guide.md +- Development practices: https://github.com/layer-3/clearsync/blob/master/contracts/solidity-development-practices.md diff --git a/.claude/rules/typescript.md b/.claude/rules/typescript.md new file mode 100644 index 000000000..d06745203 --- /dev/null +++ b/.claude/rules/typescript.md @@ -0,0 +1,13 @@ +--- +paths: + - "sdk/**/*.ts" + - "sdk/**/*.tsx" +--- + +- Use `const` by default, `let` only when reassignment is needed. No `var`. +- Prefer `viem` over `ethers.js` for Ethereum interactions in production code. +- All public API functions must be exported through the barrel `index.ts`. +- Use strict TypeScript — no `any` unless absolutely unavoidable (e.g., RPC wire types). +- Async functions preferred over raw Promise chains. +- Test files use `.test.ts` extension (not `.spec.ts`). +- When modifying sdk-compat: never barrel re-export SDK classes (SSR risk). Only `export type` is safe. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..851a6798d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,35 @@ +{ + "permissions": { + "allow": [ + "Bash(npm test*)", + "Bash(npm run build*)", + "Bash(npm run typecheck*)", + "Bash(npm run lint*)", + "Bash(npm run clean*)", + "Bash(npx jest*)", + "Bash(npx prettier*)", + "Bash(npx tsx *)", + "Bash(go test *)", + "Bash(go build *)", + "Bash(go vet *)", + "Bash(go run *)", + "Bash(forge build*)", + "Bash(forge test*)", + "Bash(forge fmt*)", + "Bash(git status*)", + "Bash(git log*)", + "Bash(git diff*)", + "Bash(git branch*)", + "Bash(gh pr *)", + "Bash(gh issue *)", + "Bash(gh run *)" + ], + "deny": [ + "Bash(git push --force*)", + "Bash(git reset --hard*)", + "Bash(rm -rf *)", + "Bash(*>.env*)", + "Bash(npm publish*)" + ] + } +} diff --git a/.claude/skills/build-sdk/SKILL.md b/.claude/skills/build-sdk/SKILL.md new file mode 100644 index 000000000..43f6700e1 --- /dev/null +++ b/.claude/skills/build-sdk/SKILL.md @@ -0,0 +1,21 @@ +--- +name: build-sdk +description: Build SDK packages in the correct dependency order +allowed-tools: Bash, Read +--- + +Build SDK packages for: $ARGUMENTS + +**Important:** sdk/ts-compat depends on sdk/ts via `"@yellow-org/sdk": "file:../ts"`. Always respect build order. + +1. If $ARGUMENTS is "all" or empty, build everything in order: + - `cd sdk/ts && npm run build` (this runs tests then tsc) + - `cd sdk/ts-compat && npm run build` + - `go build ./sdk/go/...` + +2. If $ARGUMENTS specifies a single package, build just that: + - "ts" -> `cd sdk/ts && npm run build` + - "ts-compat" or "compat" -> check sdk/ts/dist/ exists first; if not, build sdk/ts first, then `cd sdk/ts-compat && npm run build` + - "go" -> `go build ./sdk/go/...` + +3. Report build success/failure for each package with any error details. diff --git a/.claude/skills/lint/SKILL.md b/.claude/skills/lint/SKILL.md new file mode 100644 index 000000000..aa96754e2 --- /dev/null +++ b/.claude/skills/lint/SKILL.md @@ -0,0 +1,18 @@ +--- +name: lint +description: Run linters and formatters for the appropriate language +allowed-tools: Bash, Read +--- + +Lint and check formatting for: $ARGUMENTS + +Route to the correct linter based on context: + +- **sdk/ts/** -> `cd sdk/ts && npm run lint && npx prettier --check .` +- **sdk/ts-compat/** -> `cd sdk/ts-compat && npm run typecheck` (no dedicated lint script; typecheck is the closest) +- **contracts/** -> `cd contracts && forge fmt --check` +- **Go code** -> `go vet ./...` from repo root (or scope to specific path like `./sdk/go/...`) + +If no arguments provided, run all applicable linters and report a summary. + +Report any issues found. Suggest fixes but do not auto-apply unless the user asks. diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md new file mode 100644 index 000000000..4126b2082 --- /dev/null +++ b/.claude/skills/review-pr/SKILL.md @@ -0,0 +1,27 @@ +--- +name: review-pr +description: Thorough code review of current changes or a specific PR +allowed-tools: Bash, Read, Grep, Glob +--- + +Review changes for: $ARGUMENTS + +1. **Get the diff:** + - If $ARGUMENTS is a PR number: `gh pr diff $ARGUMENTS` + - If no arguments: `git diff` and `git diff --staged` for uncommitted changes + +2. **For each changed file, check:** + - **Correctness:** Does the logic do what it claims? Any off-by-one errors, race conditions, or wrong assumptions? + - **Types:** Are TypeScript types accurate and strict? Are Go errors properly handled? + - **Security:** No exposed secrets, no injection vectors, no reentrancy (contracts), no XSS + - **Tests:** Are new functions tested? Are edge cases and error paths covered? + - **Exports:** If public API changed, is the barrel `index.ts` updated? (sdk-compat: no SSR-unsafe re-exports) + - **Docs:** If behavior changed, are README/CLAUDE.md/migration docs updated? + - **Style:** Consistent with existing patterns in the file and project conventions + +3. **Categorize findings:** + - **CRITICAL:** Must fix before merge (bugs, security issues, data loss risk) + - **WARNING:** Should fix (missing tests, poor error handling, incomplete docs) + - **SUGGESTION:** Nice to have (naming improvements, minor refactors, style tweaks) + +4. **Give an overall verdict:** APPROVE / REQUEST CHANGES / COMMENT diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md new file mode 100644 index 000000000..4826a5b5c --- /dev/null +++ b/.claude/skills/test/SKILL.md @@ -0,0 +1,28 @@ +--- +name: test +description: Run tests for a specific module or the whole project +allowed-tools: Bash, Read, Grep, Glob +--- + +Run tests for: $ARGUMENTS + +Detect the target module and run the appropriate test command: + +1. If $ARGUMENTS names a specific file or directory, use that to determine the module +2. If no arguments, detect from the current working directory or recently edited files + +Route to the correct test runner: +- **sdk/ts/** -> `cd sdk/ts && npm test` +- **sdk/ts-compat/** -> `cd sdk/ts-compat && npm test` +- **sdk/go/** or Go files under sdk/go/ -> `go test ./sdk/go/... -v` (from repo root) +- **contracts/** or .sol files -> `cd contracts && forge test` +- **clearnode/** -> `go test ./clearnode/... -v` (from repo root) +- **test/integration/** -> `cd test/integration && npm test` +- **Repo root with no argument** -> run `go test ./...` (Go packages only — does NOT cover `sdk/ts`, `sdk/ts-compat`, `contracts`, or `test/integration`). Ask the user to specify a target for non-Go tests. + +If a specific test file is given (e.g., `sdk/ts/test/unit/utils.test.ts`), run only that file: +- For Jest: `cd sdk/ts && npx jest test/unit/utils.test.ts` +- For Go: resolve the file's containing package, then run `go test ./sdk/go/ -run '^TestName$'` +- For Forge: `cd contracts && forge test --match-path test/MyTest.t.sol` + +Report results: pass count, fail count, and any error details. diff --git a/.claude/skills/typecheck/SKILL.md b/.claude/skills/typecheck/SKILL.md new file mode 100644 index 000000000..2abf44f7a --- /dev/null +++ b/.claude/skills/typecheck/SKILL.md @@ -0,0 +1,18 @@ +--- +name: typecheck +description: Run TypeScript type checking across SDK packages +allowed-tools: Bash, Read +--- + +Run typecheck for: $ARGUMENTS + +1. If $ARGUMENTS specifies a package ("ts", "ts-compat", or "both"), check that package +2. If no arguments, check both TypeScript packages: + +```bash +cd sdk/ts && npm run typecheck +cd sdk/ts-compat && npm run typecheck +``` + +3. Report any type errors with file paths and line numbers +4. If the user asks about Go type checking, suggest `go vet ./...` instead diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 000000000..c14c819a5 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,82 @@ +# Nitrolite — Cursor Rules + +## What This Project Is + +Nitrolite is a state channel protocol for Ethereum/EVM blockchains. Off-chain instant transactions with on-chain security. The repo contains: Solidity contracts (Foundry), a Go clearnode broker, TypeScript + Go SDKs, and MCP servers for AI tooling. + +## Architecture + +- **Channels**: State containers between a User and a Node. Hold asset allocations, support off-chain state updates. +- **States**: Versioned allocations. Each new state = previous version + 1. Mutually signed states are enforceable on-chain. +- **Clearnode**: Off-chain broker. WebSocket JSON-RPC. Wire format: `{ "req": [REQUEST_ID, METHOD, PARAMS, TIMESTAMP], "sig": ["SIGNATURE"] }` +- **App Sessions**: Multi-party extensions on channels with quorum-based governance (weight + threshold voting). + +## TypeScript SDK Conventions + +- `@yellow-org/sdk` = v1 protocol SDK (use for all new code) +- `@yellow-org/sdk-compat` = bridges 0.5.x API surface to v1 runtime (migration only, wraps Client with NitroliteClient) +- V1 API source of truth: `docs/api.yaml`. Do NOT reference `clearnode/docs/API.md` for v1 methods (it has 0.5.x names) +- Use `const` by default, `let` only when reassignment is needed. No `var`. +- Prefer `viem` over `ethers.js` for Ethereum interactions. +- All public API functions exported through barrel `index.ts`. +- Strict TypeScript — no `any` unless unavoidable (e.g., RPC wire types). +- Async functions preferred over raw Promise chains. +- Tests: `.test.ts` extension, Jest. Run: `cd sdk/ts && npm test` +- sdk-compat: NEVER barrel re-export SDK classes (SSR risk). Only `export type` is safe. +- Build order: `sdk/ts` must build before `sdk/ts-compat`. + +## Go SDK Conventions + +- Module: `github.com/layer-3/nitrolite` (root go.mod, Go 1.25) +- Follow standard Go: `gofmt`, exported names have doc comments. +- Error handling: always check and return errors, never ignore with `_`. +- Functional options pattern for configuration (see `sdk/go/config.go`). +- Shared packages in `pkg/` — check there before creating new utilities. +- Tests: `_test.go` suffix, same package. Run from repo root: `go test ./sdk/go/...` +- Use `context.Context` for all async operations, `github.com/shopspring/decimal` for amounts. + +## Solidity Conventions + +- Foundry project: `forge build`, `forge test`, `forge fmt`. +- NatSpec comments on all public/external functions. +- Security first: validate inputs, check reentrancy, use OpenZeppelin. +- Tests in `contracts/test/` with `.t.sol` extension. +- Gas optimization matters — avoid unnecessary storage writes. +- Style guide: https://github.com/layer-3/clearsync/blob/master/contracts/solidity-style-guide.md +- Development practices: https://github.com/layer-3/clearsync/blob/master/contracts/solidity-development-practices.md + +## Key SDK Methods (Both TS and Go) + +- `Deposit(chainId, asset, amount)` — deposit funds, creates channel if needed +- `Transfer(recipient, asset, amount)` — off-chain instant transfer +- `Checkpoint(asset)` — submit co-signed state to blockchain (required after deposit, withdraw, close) +- `CloseHomeChannel(asset)` — prepare finalize state (follow with Checkpoint) +- `CreateAppSession(definition, sessionData, quorumSigs)` — create multi-party session +- `SubmitAppState(appStateUpdate, quorumSigs)` — submit app state (Operate, Withdraw, or Close intent) +- `GetChannels(wallet)`, `GetBalances(wallet)`, `GetConfig()` — queries + +> No `CloseAppSession()` exists on the SDK Client. Close via `SubmitAppState` with Close intent. + +## Key Reference Files + +- Protocol description: `protocol-description.md` +- Smart contract invariants: `contracts/SECURITY.md` +- Main SC entrypoint: `contracts/src/ChannelHub.sol` (design: `contracts/suggested-contract-design.md`) +- Clearnode logic: `clearnode/readme.md`, `clearnode/docs/` +- Advanced protocol docs: `docs/protocol/` + +## Commit Convention + +``` +feat|fix|chore|test|docs(scope): description +``` +Examples: `feat(sdk/ts): add transfer batching`, `fix(sdk-compat): export missing type` + +## Common Mistakes to Avoid + +- Don't use `ethers.js` — use `viem` for Ethereum interactions +- Don't ignore Go errors with `_` +- Don't barrel re-export classes in sdk-compat (causes SSR failures) +- Don't run `npm test && npm run build` in sdk/ts (double-tests; build already runs tests) +- Don't edit `.env` files or commit secrets +- Don't create new utilities without checking `pkg/` first (Go) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..d8a77f5c0 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,61 @@ +# Nitrolite Copilot Instructions + +Nitrolite is a state channel protocol for Ethereum/EVM blockchains — off-chain instant transactions with on-chain security. Repo contains: Solidity contracts (Foundry), Go clearnode broker, TypeScript + Go SDKs. + +## Architecture + +- Channels: state containers between User and Node, hold asset allocations +- States: versioned allocations, each version = previous + 1, mutually signed = enforceable on-chain +- Clearnode: off-chain broker, WebSocket JSON-RPC wire format +- App Sessions: multi-party extensions with quorum-based governance + +## TypeScript SDK (`@yellow-org/sdk`) + +- `@yellow-org/sdk` = v1 protocol SDK. Use for all new code. +- `@yellow-org/sdk-compat` = bridges 0.5.x API to v1 runtime. Migration only. Wraps Client with NitroliteClient. +- V1 API source of truth: `docs/api.yaml`. `clearnode/docs/API.md` has 0.5.x method names. +- Use `const` by default, `viem` over `ethers.js`, strict TypeScript (no `any`) +- All public API through barrel `index.ts`, async/await preferred +- Tests: `.test.ts`, Jest. Run: `cd sdk/ts && npm test` +- Build order: `sdk/ts` before `sdk/ts-compat` +- sdk-compat: NEVER barrel re-export classes (SSR risk), only `export type` + +## Go SDK (`github.com/layer-3/nitrolite/sdk/go`) + +- Root go.mod, Go 1.25. Standard Go: `gofmt`, doc comments on exports +- Always check errors, never ignore with `_` +- Functional options pattern (`sdk/go/config.go`), shared utils in `pkg/` +- Tests: `_test.go`, run from repo root: `go test ./sdk/go/...` +- Use `context.Context`, `github.com/shopspring/decimal` for amounts + +## Solidity (`contracts/`) + +- Foundry: `forge build`, `forge test`, `forge fmt` +- NatSpec on public/external functions, security-first, OpenZeppelin +- Tests: `.t.sol` in `contracts/test/` +- Style: https://github.com/layer-3/clearsync/blob/master/contracts/solidity-style-guide.md +- Practices: https://github.com/layer-3/clearsync/blob/master/contracts/solidity-development-practices.md + +## Key SDK Methods (both TS and Go) + +Deposit, Transfer, Checkpoint (required after deposit/withdraw/close), CloseHomeChannel (follow with Checkpoint), CreateAppSession (definition, sessionData, quorumSigs), SubmitAppState (appStateUpdate, quorumSigs — use Close intent to close sessions), GetChannels(wallet), GetBalances(wallet), GetConfig + +> No CloseAppSession() on SDK Client. Close via SubmitAppState with Close intent. + +## Key Reference Files + +- Protocol description: `protocol-description.md` +- Smart contract invariants: `contracts/SECURITY.md` +- Main SC entrypoint: `contracts/src/ChannelHub.sol` (design: `contracts/suggested-contract-design.md`) +- Clearnode logic: `clearnode/readme.md`, `clearnode/docs/` +- Advanced protocol docs: `docs/protocol/` + +## Commits + +`feat|fix|chore|test|docs(scope): description` + +## Avoid + +- ethers.js (use viem), ignoring Go errors, barrel re-exporting classes in sdk-compat +- Running `npm test && npm run build` in sdk/ts (build already runs tests) +- Editing .env files, committing secrets diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..094915426 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Claude Code (local/personal files) +.claude/settings.local.json +.claude/agent-memory/ + +# OS +.DS_Store + +# Local MCP vetting harness +sdk/mcp-test-results/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..fb3e67a43 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "github": { + "command": "gh", + "args": ["copilot", "mcp"] + }, + "nitrolite": { + "command": "npm", + "args": ["--prefix", "sdk/mcp", "exec", "--", "tsx", "sdk/mcp/src/index.ts"] + } + } +} diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 000000000..e3683450c --- /dev/null +++ b/.windsurfrules @@ -0,0 +1,82 @@ +# Nitrolite — Windsurf Rules + +## What This Project Is + +Nitrolite is a state channel protocol for Ethereum/EVM blockchains. Off-chain instant transactions with on-chain security. The repo contains: Solidity contracts (Foundry), a Go clearnode broker, TypeScript + Go SDKs, and MCP servers for AI tooling. + +## Architecture + +- **Channels**: State containers between a User and a Node. Hold asset allocations, support off-chain state updates. +- **States**: Versioned allocations. Each new state = previous version + 1. Mutually signed states are enforceable on-chain. +- **Clearnode**: Off-chain broker. WebSocket JSON-RPC. Wire format: `{ "req": [REQUEST_ID, METHOD, PARAMS, TIMESTAMP], "sig": ["SIGNATURE"] }` +- **App Sessions**: Multi-party extensions on channels with quorum-based governance (weight + threshold voting). + +## TypeScript SDK Conventions + +- `@yellow-org/sdk` = v1 protocol SDK (use for all new code) +- `@yellow-org/sdk-compat` = bridges 0.5.x API surface to v1 runtime (migration only, wraps Client with NitroliteClient) +- V1 API source of truth: `docs/api.yaml`. Do NOT reference `clearnode/docs/API.md` for v1 methods (it has 0.5.x names) +- Use `const` by default, `let` only when reassignment is needed. No `var`. +- Prefer `viem` over `ethers.js` for Ethereum interactions. +- All public API functions exported through barrel `index.ts`. +- Strict TypeScript — no `any` unless unavoidable (e.g., RPC wire types). +- Async functions preferred over raw Promise chains. +- Tests: `.test.ts` extension, Jest. Run: `cd sdk/ts && npm test` +- sdk-compat: NEVER barrel re-export SDK classes (SSR risk). Only `export type` is safe. +- Build order: `sdk/ts` must build before `sdk/ts-compat`. + +## Go SDK Conventions + +- Module: `github.com/layer-3/nitrolite` (root go.mod, Go 1.25) +- Follow standard Go: `gofmt`, exported names have doc comments. +- Error handling: always check and return errors, never ignore with `_`. +- Functional options pattern for configuration (see `sdk/go/config.go`). +- Shared packages in `pkg/` — check there before creating new utilities. +- Tests: `_test.go` suffix, same package. Run from repo root: `go test ./sdk/go/...` +- Use `context.Context` for all async operations, `github.com/shopspring/decimal` for amounts. + +## Solidity Conventions + +- Foundry project: `forge build`, `forge test`, `forge fmt`. +- NatSpec comments on all public/external functions. +- Security first: validate inputs, check reentrancy, use OpenZeppelin. +- Tests in `contracts/test/` with `.t.sol` extension. +- Gas optimization matters — avoid unnecessary storage writes. +- Style guide: https://github.com/layer-3/clearsync/blob/master/contracts/solidity-style-guide.md +- Development practices: https://github.com/layer-3/clearsync/blob/master/contracts/solidity-development-practices.md + +## Key SDK Methods (Both TS and Go) + +- `Deposit(chainId, asset, amount)` — deposit funds, creates channel if needed +- `Transfer(recipient, asset, amount)` — off-chain instant transfer +- `Checkpoint(asset)` — submit co-signed state to blockchain (required after deposit, withdraw, close) +- `CloseHomeChannel(asset)` — prepare finalize state (follow with Checkpoint) +- `CreateAppSession(definition, sessionData, quorumSigs)` — create multi-party session +- `SubmitAppState(appStateUpdate, quorumSigs)` — submit app state (Operate, Withdraw, or Close intent) +- `GetChannels(wallet)`, `GetBalances(wallet)`, `GetConfig()` — queries + +> No `CloseAppSession()` exists on the SDK Client. Close via `SubmitAppState` with Close intent. + +## Key Reference Files + +- Protocol description: `protocol-description.md` +- Smart contract invariants: `contracts/SECURITY.md` +- Main SC entrypoint: `contracts/src/ChannelHub.sol` (design: `contracts/suggested-contract-design.md`) +- Clearnode logic: `clearnode/readme.md`, `clearnode/docs/` +- Advanced protocol docs: `docs/protocol/` + +## Commit Convention + +``` +feat|fix|chore|test|docs(scope): description +``` +Examples: `feat(sdk/ts): add transfer batching`, `fix(sdk-compat): export missing type` + +## Common Mistakes to Avoid + +- Don't use `ethers.js` — use `viem` for Ethereum interactions +- Don't ignore Go errors with `_` +- Don't barrel re-export classes in sdk-compat (causes SSR failures) +- Don't run `npm test && npm run build` in sdk/ts (double-tests; build already runs tests) +- Don't edit `.env` files or commit secrets +- Don't create new utilities without checking `pkg/` first (Go) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..4a09848b4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ +# Nitrolite + +Nitrolite is a state channel framework for Ethereum and EVM-compatible blockchains. It enables off-chain interactions (instant finality, low gas) while maintaining on-chain security guarantees. + +## Repository Structure + +| Directory | Description | Language | +|-----------|-------------|----------| +| `contracts/` | Solidity smart contracts (ChannelHub, ChannelEngine) | Solidity (Foundry) | +| `clearnode/` | Off-chain broker: ledger services, WebSocket JSON-RPC | Go | +| `sdk/ts/` | TypeScript SDK (`@yellow-org/sdk`) | TypeScript | +| `sdk/ts-compat/` | Compat layer (`@yellow-org/sdk-compat`) bridging v0.5.3 API to v1.0.0+ | TypeScript | +| `sdk/go/` | Go SDK for backend integrations | Go | +| `sdk/mcp/` | Unified MCP server — TypeScript + Go SDK context for AI agents/IDEs | TypeScript | +| `cerebro/` | Interactive CLI for channel/asset management | Go | +| `pkg/` | Shared Go packages (core, sign, rpc, app, blockchain, log) | Go | +| `docs/` | Protocol specifications, architecture docs | Markdown | +| `test/integration/` | Integration tests against a live clearnode | TypeScript | + +See stack-specific `CLAUDE.md` files in `sdk/ts/`, `sdk/ts-compat/`, and `sdk/go/` for detailed conventions. + +## Build & Test Commands + +### TypeScript SDK +```bash +cd sdk/ts && npm install # Install dependencies (first time) +cd sdk/ts && npm test # Unit tests (Jest) +cd sdk/ts && npm run build # Tests + compile (runs tests first!) +cd sdk/ts && npm run typecheck # Type check only +cd sdk/ts && npm run lint # ESLint +``` + +### TypeScript SDK Compat +```bash +cd sdk/ts-compat && npm install # Install dependencies (first time) +cd sdk/ts-compat && npm test # Unit tests (Jest) +cd sdk/ts-compat && npm run build # Compile +cd sdk/ts-compat && npm run typecheck # Type check only +``` + +### Go SDK +```bash +go test ./sdk/go/... -v # SDK tests only (from repo root) +go build ./sdk/go/... # Build SDK +go test ./... # ALL Go tests (clearnode + pkg + sdk + cerebro) +go vet ./... # Lint all Go code +``` + +### Smart Contracts +```bash +cd contracts && forge build # Compile +cd contracts && forge test # Run tests +cd contracts && forge fmt # Format +``` + +### Integration Tests +```bash +cd test/integration && npm test # Requires a running clearnode +``` + +## Important Notes + +- **Go module** is at repo root: `go.mod`, module `github.com/layer-3/nitrolite`, Go 1.25 +- **Build order**: `sdk/ts` must build before `sdk/ts-compat` (has `"@yellow-org/sdk": "file:../ts"` dependency) +- **sdk/ts build runs tests first**: `npm run build` = `npm run test && tsc`. Avoid `npm test && npm run build` (double-tests). +- **Foundry** uses git submodules for deps (`forge-std`, `openzeppelin-contracts`). Use `--recurse-submodules` on clone. +- **MCP server** (`sdk/mcp/`): run `cd sdk/mcp && npm install` before first use. +- **Never** edit `.env` files or commit secrets. + +## V1 Protocol Source of Truth + +- **API definition**: `docs/api.yaml` — canonical list of all v1 RPC methods, types, and request/response schemas +- **Protocol spec**: `docs/protocol/` — state channels, transitions, enforcement, security +- **Contract invariants**: `contracts/SECURITY.md` +- **Contract design**: `contracts/suggested-contract-design.md`, entrypoint `contracts/src/ChannelHub.sol` +- **Clearnode docs**: `clearnode/readme.md`, `clearnode/docs/` + +**Do NOT use `clearnode/docs/API.md` as v1 reference** — it documents the 0.5.x compat-layer method names (e.g., `transfer`, `create_channel`, `auth_request`). The v1 methods use grouped names (e.g., `channels.v1.submit_state`, `app_sessions.v1.create_app_session`). + +### SDK vs Compat + +- **`@yellow-org/sdk`** (`sdk/ts/`) — v1 protocol SDK. Use for all new code. +- **`@yellow-org/sdk-compat`** (`sdk/ts-compat/`) — bridges 0.5.x API surface to v1 runtime. Wraps `Client` with `NitroliteClient`, exposes legacy types and method names. For migration only. +- **`sdk/go/`** — Go v1 SDK. No compat layer exists for Go. + +## Commit Convention + +```text +feat|fix|chore|test|docs(scope): description + +# Examples: +feat(sdk/ts): add transfer batching support +fix(sdk-compat): export missing generateRequestId +chore(contracts): update OpenZeppelin to v5.2 +test(integration): add channel resize test +``` + +## CI Workflows + +| Workflow | Trigger | What it tests | +|----------|---------|---------------| +| `test-go.yml` | PR / push | Go tests (`go test ./...`) | +| `test-forge.yml` | PR / push | Contract tests (`forge test`) | +| `test-sdk.yml` | push | TypeScript SDK tests | +| `test-integration.yml` | push | Integration tests | +| `publish-sdk.yml` | release | Publish SDK to npm | diff --git a/llms-full.txt b/llms-full.txt new file mode 100644 index 000000000..616932959 --- /dev/null +++ b/llms-full.txt @@ -0,0 +1,385 @@ +# Nitrolite — Full AI Reference + +This document provides a comprehensive reference for AI agents and LLMs working with the Nitrolite state channel protocol and SDKs. It is designed to be loaded as context in Claude Projects, ChatGPT, or any AI tool. + +--- + +## 1. What is Nitrolite? + +Nitrolite is a state channel protocol for Ethereum and EVM-compatible blockchains. It enables high-speed off-chain interactions (instant finality, near-zero gas) while preserving on-chain security guarantees. + +Users exchange signed state updates off-chain with Nodes that act as a hub. Any user can enforce the latest agreed state on the blockchain at any time. + +### System Roles + +- **User** — Opens channels, signs state updates, holds assets within the protocol +- **Node (Clearnode)** — Facilitates off-chain state advancement, manages channels, syncs with blockchain +- **Blockchain** — On-chain storage/execution layer that validates states, stores checkpoints, resolves disputes + +### Design Goals + +- Off-chain scalability — minimize on-chain transactions +- Blockchain security — any user can fall back to on-chain enforcement +- Cross-chain asset interaction — unified asset model across blockchains +- Extensibility — app sessions, custom logic via extensions + +--- + +## 2. Core Concepts + +### Channels + +A channel is a state container shared between a User and a Node. It holds asset allocations and supports off-chain state updates. Each channel is defined by immutable parameters: user, node, asset, nonce, challenge duration, and approved signature validators. + +### States + +A state represents the current agreed asset allocations. Contains two ledgers (home and non-home), a version number, and a transition describing the operation that produced it. + +### State Advancement + +User and node advance states off-chain by exchanging signed transitions. Each new state MUST have version = previous + 1. Transitions include: deposit, withdrawal, transfer, commit, release, escrow operations, migration. + +### State Enforcement + +Any party MAY submit the latest signed state to the blockchain. The blockchain validates signatures, version ordering, and ledger invariants before accepting. + +### App Sessions + +Multi-party extensions on top of channels with quorum-based governance. Support custom application logic (gaming, escrow, multi-party coordination). Use weight-based voting: each participant has a signature weight, operations require meeting a quorum threshold. + +--- + +## 3. Wire Format + +Communication uses JSON-RPC over WebSocket. + +### Message Envelope + +The outer key (`req`, `res`) indicates message type. The inner tuple is a 4-element ordered array: + +`[RequestId, Method, Payload, Timestamp]` + +| Field | Description | +|-------|-------------| +| RequestId | Numeric identifier unique per connection | +| Method | Operation name (e.g., "channels.v1.submit_state", "node.v1.ping") | +| Payload | Method-specific params (object or array) | +| Timestamp | Unix milliseconds | + +### Request Format +```json +{ "req": [REQUEST_ID, METHOD, PARAMS, TIMESTAMP], "sig": ["SIGNATURE"] } +``` + +### Response Format +```json +{ "res": [REQUEST_ID, METHOD, DATA, TIMESTAMP], "sig": ["SIGNATURE"] } +``` + +The `sig` field contains the sender's signature over the entire `req` or `res` tuple. + +--- + +## 4. Request Signing + +Every v1 RPC request includes a `sig` field — the client's signature over the entire `req` tuple. This is the authorization mechanism. There is no separate authentication handshake in v1; request signatures are the identity proof. + +### Session Keys + +Session keys enable delegated signing with scoped permissions. They are registered and managed via `channels.v1.submit_session_key_state` and `app_sessions.v1.submit_session_key_state`. Session keys have: +- Per-asset allowances with spending caps +- Expiration timestamps +- Scoping to specific applications and app sessions + +--- + +## 5. Channel Lifecycle + +### Definition Parameters + +| Field | Description | +|-------|-------------| +| User | User participant address | +| Node | Node participant address | +| Asset | Asset identifier | +| Nonce | Unique nonce for the channel | +| ChallengeDuration | Challenge period in seconds | +| ApprovedSignatureValidators | Bitmask of approved validation modes | + +### Lifecycle + +1. **Create** — Off-chain: validate, construct initial state, sign +2. **Advance** — Off-chain: exchange signed state transitions (deposit, withdraw, transfer) +3. **Checkpoint** — Submit mutually signed state to blockchain +4. **Challenge** — On-chain dispute: submit state, wait challenge duration +5. **Close** — Off-chain mutual agreement OR on-chain post-challenge + +### Transition Types + +- **Local:** HomeDeposit, HomeWithdrawal, Finalize +- **Transfer:** TransferSend, TransferReceive, Acknowledgement +- **Extension:** Commit, Release (bridge to app sessions) +- **Escrow:** EscrowDepositInitiate/Finalize, EscrowWithdrawalInitiate/Finalize (cross-chain deposit/withdrawal) +- **Migration:** InitiateMigration, FinalizeMigration (move channel's home chain to another blockchain) + +--- + +## 6. State Invariants + +- **Fund conservation:** UserAllocation + NodeAllocation == UserNetFlow + NodeNetFlow +- **Non-negativity:** All allocation values MUST be >= 0 +- **Version ordering:** Off-chain: version = previous + 1. On-chain: submitted > current +- **Version uniqueness:** No two different states with the same version can exist for the same channel +- **Signature requirements:** Mutually signed (user + node) = enforceable. Node-only = NOT enforceable. +- **Locked funds:** UserAllocation + NodeAllocation == LockedFunds (unless closing) +- **Allocation backing:** Sum of allocations must equal locked collateral +- **No retrogression:** State with version <= lastEnforcedVersion cannot be enforced on-chain +- **Cross-deployment replay protection:** ChannelHub VERSION encoded as first byte of channelId + +For the full list of protocol and smart contract invariants, see `contracts/SECURITY.md`. + +--- + +## 7. Clearnode V1 RPC Methods + +Canonical reference: `docs/api.yaml`. Methods are grouped by domain with `v1` versioning. + +### channels.v1 + +| Method | Description | +|--------|-------------| +| `get_home_channel` | Retrieve home channel for a wallet + asset | +| `get_escrow_channel` | Retrieve escrow channel by ID | +| `get_channels` | List all channels for a user with optional filtering | +| `get_latest_state` | Retrieve latest state for a wallet + asset | +| `get_states` | Retrieve multiple states with filtering | +| `request_creation` | Request channel creation (returns signed initial state) | +| `submit_state` | Submit a signed state transition (deposit, withdraw, transfer, close, resize) | +| `submit_session_key_state` | Register/update a channel session key | +| `get_last_key_states` | Get latest channel session key states | + +### app_sessions.v1 + +| Method | Description | +|--------|-------------| +| `create_app_session` | Create a new app session between participants | +| `submit_app_state` | Submit app state update (Operate, Withdraw, or Close intent) | +| `submit_deposit_state` | Submit a deposit into an app session | +| `rebalance_app_sessions` | Atomic rebalancing across multiple app sessions | +| `get_app_definition` | Retrieve app definition for a session | +| `get_app_sessions` | List app sessions with optional filtering | +| `submit_session_key_state` | Register/update an app session key | +| `get_last_key_states` | Get latest app session key states | + +### apps.v1 + +| Method | Description | +|--------|-------------| +| `get_apps` | List registered applications | +| `submit_app_version` | Register a new application (owner must sign packed app data) | + +### user.v1 + +| Method | Description | +|--------|-------------| +| `get_balances` | Retrieve user balances | +| `get_transactions` | Retrieve transaction history with filtering | +| `get_action_allowances` | Retrieve action allowances based on staking level | + +### node.v1 + +| Method | Description | +|--------|-------------| +| `ping` | Connectivity check | +| `get_config` | Node configuration and supported blockchains | +| `get_assets` | Supported assets (optionally filtered by blockchain) | + +> There is no `close_app_session` method. App sessions are closed via `submit_app_state` with Close intent. +> Channel operations (transfer, close, resize) are all handled via `submit_state` with different transition types. + +--- + +## 8. TypeScript SDK (`@yellow-org/sdk`) + +Package: `@yellow-org/sdk` on npm. Uses viem, decimal.js, async/await. + +### Client Creation + +```typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; + +const { stateSigner, txSigner } = createSigners(privateKey); +const client = await Client.create( + clearnodeUrl, + stateSigner, + txSigner, + withBlockchainRPC(chainId, rpcUrl), +); +``` + +### Key Methods + +| Method | Description | +|--------|-------------| +| `deposit(chainId, asset, amount: Decimal)` | Deposit funds (creates channel if needed) | +| `transfer(recipient, asset, amount: Decimal)` | Off-chain instant transfer | +| `checkpoint(asset)` | Submit co-signed state to blockchain (required after deposit, withdraw, close) | +| `closeHomeChannel(asset)` | Prepare finalize state (follow with checkpoint) | +| `approveToken(chainId, asset, amount: Decimal)` | Approve token spending (ERC-20 only) | +| `getChannels(wallet, options?)` | List channels for a wallet | +| `getBalances(wallet)` | Get account balances | +| `getConfig()` | Get clearnode configuration | +| `createAppSession(definition, sessionData, quorumSigs)` | Create app session | +| `submitAppState(appStateUpdate, quorumSigs)` | Submit app state (Operate, Withdraw, or Close intent) | +| `submitAppSessionDeposit(appStateUpdate, quorumSigs, asset, amount)` | Deposit into app session | +| `rebalanceAppSessions(signedUpdates)` | Atomic rebalancing across multiple app sessions | + +### Security Token Locking (on-chain escrow) + +| Method | Description | +|--------|-------------| +| `escrowSecurityTokens(targetWallet, chainId, amount)` | Lock tokens in the Locking contract | +| `initiateSecurityTokensWithdrawal(chainId)` | Initiate unlock of escrowed tokens | +| `cancelSecurityTokensWithdrawal(chainId)` | Cancel pending unlock | +| `withdrawSecurityTokens(chainId, destination)` | Withdraw unlocked tokens | +| `approveSecurityToken(chainId, amount)` | Approve Locking contract to spend tokens | +| `getLockedBalance(chainId, wallet)` | Get locked balance for a wallet | +| `getEscrowChannel(escrowChannelId)` | Get escrow channel info | + +> Note: There is no `closeAppSession()` on the SDK Client. Close a session by calling `submitAppState` with Close intent. + +### Compat Layer (`@yellow-org/sdk-compat`) + +The compat layer (`@yellow-org/sdk-compat`) bridges the gap between the 0.5.x API surface and the v1 protocol SDK. It exists because dApps built on the old `@layer-3/nitrolite` v0.5.3 package used a different API shape: `NitroliteClient` instead of `Client.create`, legacy method names like `transfer`/`create_channel`/`close_app_session` instead of v1 grouped methods, and different type structures. + +The compat layer wraps the v1 `Client` internally but exposes the old 0.5.x API surface, so existing dApps can upgrade the underlying SDK without rewriting all their code at once. It re-exports legacy types (`RPCMethod`, `RPCChannelStatus`, `NitroliteRPCMessage`) and provides helper functions (`createTransferMessage`, `createAuthRequestMessage`) that map to v1 calls internally. + +**For new projects, always use `@yellow-org/sdk` directly.** The compat layer is for migration only. + +--- + +## 9. Go SDK (`github.com/layer-3/nitrolite/sdk/go`) + +Uses context.Context, (T, error) tuples, github.com/shopspring/decimal. + +### Client Creation + +```go +import ( + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" +) + +stateSigner, _ := sign.NewEthereumMsgSigner(privateKey) +txSigner, _ := sign.NewEthereumRawSigner(privateKey) + +client, err := sdk.NewClient(clearnodeURL, stateSigner, txSigner, + sdk.WithBlockchainRPC(chainID, rpcURL), +) +defer client.Close() +``` + +### Key Methods + +| Method | Description | +|--------|-------------| +| `Deposit(ctx, chainID, asset, amount)` | Deposit funds (creates channel if needed) | +| `Transfer(ctx, recipient, asset, amount)` | Off-chain transfer | +| `Checkpoint(ctx, asset)` | Submit co-signed state to blockchain (required after deposit, withdraw, close) | +| `CloseHomeChannel(ctx, asset)` | Prepare finalize state (follow with Checkpoint) | +| `ApproveToken(ctx, chainID, asset, amount)` | Approve token spending (ERC-20 only) | +| `GetChannels(ctx, wallet, options)` | List channels for a wallet | +| `GetBalances(ctx, wallet)` | Get balances | +| `GetConfig(ctx)` | Get clearnode config | +| `CreateAppSession(ctx, def, sessionData, quorumSigs)` | Create app session | +| `SubmitAppState(ctx, appStateUpdate, quorumSigs)` | Submit app state (Operate, Withdraw, or Close intent) | +| `SubmitAppSessionDeposit(ctx, appStateUpdate, quorumSigs, asset, amount)` | Deposit into app session | +| `RebalanceAppSessions(ctx, signedUpdates)` | Atomic rebalancing across multiple app sessions | + +### Security Token Locking (on-chain escrow) + +| Method | Description | +|--------|-------------| +| `EscrowSecurityTokens(ctx, targetWallet, chainID, amount)` | Lock tokens in the Locking contract | +| `InitiateSecurityTokensWithdrawal(ctx, chainID)` | Initiate unlock | +| `CancelSecurityTokensWithdrawal(ctx, chainID)` | Cancel pending unlock | +| `WithdrawSecurityTokens(ctx, chainID, destination)` | Withdraw unlocked tokens | +| `ApproveSecurityToken(ctx, chainID, amount)` | Approve Locking contract spend | +| `GetLockedBalance(ctx, chainID, wallet)` | Get locked balance | +| `GetEscrowChannel(ctx, escrowChannelID)` | Get escrow channel info | + +> Note: There is no `CloseAppSession()` on the Go Client. Close a session by calling `SubmitAppState` with Close intent. + +### Two-Step Pattern for Blockchain Operations + +1. Build and co-sign state off-chain (Deposit, Withdraw, etc.) +2. Settle on-chain via Checkpoint + +--- + +## 10. Smart Contracts + +Foundry project. Key contracts: + +| Contract | Purpose | +|----------|---------| +| `ChannelHub.sol` | Main entry point — creates channels, processes checkpoints | +| `ChannelEngine.sol` | State validation, transition processing | +| `EscrowDepositEngine.sol` | Cross-chain deposit escrow | +| `EscrowWithdrawalEngine.sol` | Cross-chain withdrawal escrow | +| `SessionKeyValidator.sol` | Validates session key signatures | +| `ECDSAValidator.sol` | Validates standard ECDSA signatures | + +### Build and Test + +```bash +cd contracts && forge build # Compile +cd contracts && forge test # Run tests +cd contracts && forge fmt # Format +``` + +--- + +## 11. Repository Structure + +| Directory | Description | Language | +|-----------|-------------|----------| +| `contracts/` | Solidity smart contracts (Foundry) | Solidity | +| `clearnode/` | Off-chain broker: WebSocket JSON-RPC | Go | +| `sdk/ts/` | TypeScript SDK (`@yellow-org/sdk`) | TypeScript | +| `sdk/ts-compat/` | Compat layer for v0.5.3 migration | TypeScript | +| `sdk/go/` | Go SDK | Go | +| `sdk/mcp/` | Unified MCP server for AI agents | TypeScript | +| `cerebro/` | Interactive CLI for channel management | Go | +| `pkg/` | Shared Go packages (core, sign, rpc, app) | Go | +| `docs/` | Protocol specifications | Markdown | + +--- + +## 12. Common Patterns + +### TypeScript: Transfer with Compat Layer + +```typescript +import { NitroliteClient } from '@yellow-org/sdk-compat'; +const client = new NitroliteClient(url, stateSigner, txSigner, options); +await client.transfer(recipient, 'usdc', '5.0'); +``` + +### Go: Full Transfer Flow + +```go +ctx := context.Background() +client.Deposit(ctx, 11155111, "usdc", decimal.NewFromInt(10)) +client.Checkpoint(ctx, "usdc") // settle deposit on-chain +client.Transfer(ctx, recipient, "usdc", decimal.NewFromInt(5)) +client.CloseHomeChannel(ctx, "usdc") +client.Checkpoint(ctx, "usdc") // settle close on-chain +``` + +### App Session: 2-of-2 Quorum + +Both SDKs support app sessions with weight-based quorum: +- Participants: [{addr1, weight: 50}, {addr2, weight: 50}], Quorum: 100 +- Both must sign to authorize state updates +- Variations: 2-of-3 (weights [50,50,50], quorum 100), weighted operator (weights [70,30], quorum 70) diff --git a/llms.txt b/llms.txt new file mode 100644 index 000000000..69a19efb2 --- /dev/null +++ b/llms.txt @@ -0,0 +1,46 @@ +# Nitrolite + +> State channel protocol for Ethereum and EVM-compatible blockchains. Instant off-chain transactions with on-chain security guarantees. + +## Documentation + +- [Protocol Overview](https://github.com/layer-3/nitrolite/blob/main/docs/protocol/overview.md): Design goals, system roles, core concepts +- [Terminology](https://github.com/layer-3/nitrolite/blob/main/docs/protocol/terminology.md): Canonical definitions of all protocol terms +- [State Model](https://github.com/layer-3/nitrolite/blob/main/docs/protocol/state-model.md): State structure, versioning, consistency rules +- [Channel Protocol](https://github.com/layer-3/nitrolite/blob/main/docs/protocol/channel-protocol.md): Channel lifecycle, transitions, advancement rules +- [Cryptography](https://github.com/layer-3/nitrolite/blob/main/docs/protocol/cryptography.md): Encoding, hashing, signing, replay protection +- [Enforcement](https://github.com/layer-3/nitrolite/blob/main/docs/protocol/enforcement.md): On-chain checkpoints, validation, dispute resolution +- [Interactions](https://github.com/layer-3/nitrolite/blob/main/docs/protocol/interactions.md): Message envelope, wire format, operations +- [Security](https://github.com/layer-3/nitrolite/blob/main/docs/protocol/security-and-limitations.md): Security guarantees, trust assumptions, known limitations +- [Cross-Chain](https://github.com/layer-3/nitrolite/blob/main/docs/protocol/cross-chain-and-assets.md): Unified asset model, cross-chain operations + +## SDKs + +- [TypeScript SDK](https://github.com/layer-3/nitrolite/tree/main/sdk/ts): `@yellow-org/sdk` — async/await, viem, decimal.js +- [TypeScript Compat](https://github.com/layer-3/nitrolite/tree/main/sdk/ts-compat): `@yellow-org/sdk-compat` — migration bridge from v0.5.3 +- [Go SDK](https://github.com/layer-3/nitrolite/tree/main/sdk/go): `github.com/layer-3/nitrolite/sdk/go` — context.Context, (T, error) tuples + +## Clearnode API + +- [API Reference](https://github.com/layer-3/nitrolite/blob/main/clearnode/docs/API.md): All RPC methods with request/response examples +- [Protocol Spec](https://github.com/layer-3/nitrolite/blob/main/clearnode/docs/Clearnode.protocol.md): Auth flow, channel creation, app sessions +- [Entities](https://github.com/layer-3/nitrolite/blob/main/clearnode/docs/Entities.md): Data entity definitions +- [Session Keys](https://github.com/layer-3/nitrolite/blob/main/clearnode/docs/SessionKeys.md): Delegated signing with spending caps + +## Smart Contracts + +- [Contracts Source](https://github.com/layer-3/nitrolite/tree/main/contracts/src): ChannelHub, ChannelEngine, EscrowDepositEngine, signature validators +- [Contract Design](https://github.com/layer-3/nitrolite/blob/main/contracts/suggested-contract-design.md): Architecture rationale and unified transition pattern + +## AI Tooling + +- [TypeScript MCP Server](https://github.com/layer-3/nitrolite/tree/main/sdk/ts-mcp): MCP server for TS SDK + protocol knowledge (22 resources, 8 tools, 3 prompts) +- [Go MCP Server](https://github.com/layer-3/nitrolite/tree/main/sdk/mcp-go): MCP server for Go SDK + protocol knowledge (16 resources, 5 tools, 2 prompts) + +## Key Concepts + +- **State Channels**: Off-chain state containers between a user and a node. Signed state updates exchanged off-chain; any party can enforce on-chain. +- **Clearnode**: Off-chain broker that manages channels, processes RPC requests, and syncs with blockchain. +- **Wire Format**: JSON-RPC over WebSocket. Request: `{ "req": [REQUEST_ID, METHOD, PARAMS, TIMESTAMP], "sig": ["SIGNATURE"] }` +- **Authorization**: Every RPC request is signed via the `sig` field. Session keys enable delegated signing with spending caps. +- **App Sessions**: Multi-party extensions on top of channels with quorum-based governance and custom application logic. diff --git a/sdk/THE_WAY_AHEAD.md b/sdk/THE_WAY_AHEAD.md new file mode 100644 index 000000000..cf8a4c327 --- /dev/null +++ b/sdk/THE_WAY_AHEAD.md @@ -0,0 +1,311 @@ +# The Way Ahead: Productionalising Nitrolite AI Tooling + +This document is the roadmap for taking our MCP servers from "works if you clone the repo" to "any developer, anywhere, gets Nitrolite AI tooling with one command." It's written for someone who has never published an npm package or a Go binary before. + +--- + +## Where We Are + +| What we have | Status | +|---|---| +| Unified MCP server (`sdk/mcp/`) | Works locally, 30 resources, 8 tools, 3 prompts — covers TypeScript and Go SDKs | +| Protocol docs, clearnode API, SDK source | Indexed at startup by the MCP server | + +**The problem:** The server reads files from the repo at runtime (`docs/protocol/`, `docs/api.yaml`, `sdk/ts/src/`, `sdk/go/`). If you don't have the full repo cloned, it doesn't work. No external developer can use it. + +--- + +## IMPORTANT: Release Parity + +**The MCP server currently reads from repo source on disk, not from the published SDK release.** This means: + +- The MCP parses `sdk/ts/src/client.ts` directly — which may contain unreleased methods not yet in `@yellow-org/sdk` on npm +- The MCP parses `sdk/go/*.go` — same issue, may expose unreleased Go API surface +- Protocol docs in `docs/protocol/` may document unreleased behavior + +**This is fine for internal development** (you want AI to know about in-progress code). But for the published MCP package, this creates a real problem: an AI agent could tell an external developer "use `client.someNewMethod()`" — but when they `npm install @yellow-org/sdk`, that method doesn't exist yet. + +**When publishing (Phase 1 below), you MUST:** + +1. **Embed content from the tagged release commit, not from main.** The CI workflow that publishes the MCP package should check out the corresponding SDK release tag (e.g., `sdk/ts/v1.2.0`) and embed content from that snapshot. +2. **Version-lock the MCP to its SDK.** The MCP package version should track the SDK version it documents. If `@yellow-org/sdk` is at `1.2.0`, the MCP should be `1.2.0` too (or `1.2.0-mcp.1` if iterating on MCP-only changes). +3. **Add a version check at startup.** When running locally from repo source (not published), the MCP should log a warning: "Reading from source — API surface may differ from published SDK." +4. **Coordinate release timing.** Publish the MCP package as part of the same release workflow that publishes the SDK, so they're always in sync. + +This is the single most important detail to get right before publishing. An MCP that tells developers about methods they can't use is worse than no MCP at all. + +--- + +## Phase 1: Publish TS MCP to npm + +**Goal:** Any developer runs `npx @yellow-org/sdk-mcp` and gets the full Nitrolite MCP server. + +### Step 1: Embed content at build time + +The server currently reads files from disk. For npm, we need to bundle all content into the compiled JavaScript. Two approaches: + +**Option A (simpler):** A build script that reads all protocol docs, examples, and SDK source, then generates a `src/embedded-content.ts` file with all content as exported string constants. The server imports from this file instead of using `fs.readFileSync`. + +**Option B:** Use a bundler like `tsup` or `esbuild` with a plugin that inlines file reads at build time. + +Option A is recommended — it's explicit, debuggable, and doesn't add build tool complexity. + +### Step 2: Set up package.json for publishing + +Current `package.json` needs these changes: + +```jsonc +{ + "name": "@yellow-org/sdk-mcp", + "version": "0.1.0", + "description": "MCP server exposing the Nitrolite SDK to AI agents and IDEs", + "main": "dist/index.js", // compiled output, not .ts source + "types": "dist/index.d.ts", + "bin": { + "nitrolite-sdk-mcp": "dist/index.js" + }, + "files": [ // what goes in the npm tarball + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" // scoped packages default to private! + }, + "repository": { + "type": "git", + "url": "https://github.com/layer-3/nitrolite.git", + "directory": "sdk/mcp" + }, + "license": "MIT", + "scripts": { + "build": "tsc", + "prepublishOnly": "npm run build" // auto-builds before publish + } +} +``` + +**Important:** Keep the shebang (`#!/usr/bin/env node`) at the top of `sdk/mcp/src/index.ts`. TypeScript preserves shebangs when emitting JavaScript, so `tsc` will carry it through to `dist/index.js` and the `bin` entry will remain executable. `tsup` is still a reasonable alternative build tool here, but it is optional rather than required for shebang handling. + +### Step 3: Create an npm account and get access + +1. **Create account:** Go to https://www.npmjs.com/signup +2. **Enable 2FA:** Avatar > Account Settings > Two-Factor Authentication. Scan QR code with an authenticator app. This is mandatory for publishing. +3. **Get org access:** The `@yellow-org` org already exists (it publishes `@yellow-org/sdk`). An existing org admin must invite you at https://www.npmjs.com/settings/yellow-org/members with the "developer" role (can publish packages). + +### Step 4: Publish manually (first time) + +```bash +# Login to npm (opens browser for auth) +npm login + +# Verify you're logged in +npm whoami + +# Go to the package directory +cd sdk/mcp + +# Build +npm run build + +# Preview what will be in the tarball (sanity check) +npm pack --dry-run + +# Publish! --access public is critical for scoped packages +npm publish --access public +``` + +After this, anyone in the world can run: +```bash +npx @yellow-org/sdk-mcp +``` + +### Step 5: Automate publishing with GitHub Actions + +After the first manual publish, set up automation so future versions publish on git tag. + +**Option A: Trusted Publishing (recommended, no secrets needed)** + +1. On npmjs.com, go to the package > Access > Trusted Publishers > Add GitHub Actions +2. Enter: org=`layer-3`, repo=`nitrolite`, workflow=`publish-sdk-mcp.yml` +3. Create the workflow: + +```yaml +# .github/workflows/publish-sdk-mcp.yml +name: Publish @yellow-org/sdk-mcp + +on: + push: + tags: + - 'sdk-mcp/v*' # triggers on tags like sdk-mcp/v0.2.0 + +permissions: + contents: read + id-token: write # required for OIDC auth with npm + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + - run: npm ci + working-directory: ./sdk/mcp + - run: npm run build + working-directory: ./sdk/mcp + - run: npm publish --access public --provenance + working-directory: ./sdk/mcp + # No NODE_AUTH_TOKEN needed! OIDC handles it. +``` + +4. To publish a new version: +```bash +# Bump version in package.json +cd sdk/mcp +npm version patch # or minor, or major + +# Tag and push +git add sdk/mcp/package.json +git commit -m "chore(sdk-mcp): bump to v0.2.0" +git tag sdk-mcp/v0.2.0 +git push origin main +git push origin sdk-mcp/v0.2.0 +# GitHub Actions takes over from here +``` + +**Option B: npm token (fallback)** + +If trusted publishing doesn't work for your setup: +1. Create a granular access token at npmjs.com > Access Tokens +2. Scope it to `@yellow-org/sdk-mcp` only, with "Require 2FA" disabled (so CI can use it) +3. Add it as `NPM_TOKEN` in GitHub repo secrets +4. Add to the publish step: `env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` + +### Step 6: Submit to the official MCP Registry + +The MCP Registry (registry.modelcontextprotocol.io) is a discovery directory. Getting listed means AI tools can find your server automatically. + +```bash +# Install the publisher CLI +brew install modelcontextprotocol/tap/mcp-publisher + +# Generate server.json in sdk/mcp/ +cd sdk/mcp +mcp-publisher init +``` + +Edit `server.json`: +```json +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.layer-3/nitrolite-sdk-mcp", + "description": "MCP server exposing the Nitrolite state channel SDK to AI agents", + "version": "0.1.0", + "repository": { + "url": "https://github.com/layer-3/nitrolite", + "source": "github", + "id": "layer-3/nitrolite", + "subfolder": "sdk/mcp" + }, + "packages": [{ + "registry_type": "npm", + "name": "@yellow-org/sdk-mcp", + "version": "0.1.0", + "runtime": "node", + "package_arguments": [], + "environment_variables": [] + }] +} +``` + +```bash +# Authenticate with GitHub +mcp-publisher login github # opens browser + +# Publish to registry +mcp-publisher publish +``` + +Verify: `curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=nitrolite"` + +**When updating:** Bump both `version` and `packages[].version` in `server.json`, then run `mcp-publisher publish` again. + +### Common npm gotchas + +- Scoped packages (`@yellow-org/*`) default to **private**. Always use `--access public` on first publish or set `publishConfig.access` in package.json +- `npm pack --dry-run` shows exactly what goes in the tarball — use it before every publish +- You **cannot unpublish** a version after 72 hours. Version numbers are permanent. +- npm requires 2FA for all publishing since late 2025 + +--- + +Go SDK context is now served by the unified TypeScript MCP server (`sdk/mcp`). A separate Go binary distribution is no longer planned — the single npm-published `@yellow-org/sdk-mcp` package covers both TypeScript and Go SDK surfaces. + +--- + +## Phase 2: Cross-IDE Context Files + +These are simple text files that make the repo AI-friendly for every coding tool — not just Claude Code. + +| File | Tool | What it does | +|------|------|-------------| +| `llms.txt` | Any LLM / web crawler | Machine-readable summary of the project | +| `llms-full.txt` | Claude Projects, ChatGPT | Full protocol + SDK reference in one file | +| `.cursorrules` | Cursor | Project-specific AI coding rules | +| `.github/copilot-instructions.md` | GitHub Copilot | Org-wide coding conventions (4K char limit) | +| `.windsurfrules` | Windsurf | Same as cursorrules | + +These are low-effort, high-reach. Content already exists in `.claude/rules/` and `CLAUDE.md` — it just needs to be reformatted. + +--- + +## Phase 3: Agent Skills (SKILL.md) + +The SKILL.md format (from Anthropic, adopted by OpenAI Codex and VS Code Copilot) teaches AI agents *how to do things* — complementary to MCP which provides *tools*. + +Create a `skills/` directory or separate repo (`yellow-org/nitrolite-skills`) with: + +| Skill | What it teaches | +|-------|----------------| +| `build-transfer-app` | How to build a token transfer app from scratch | +| `build-app-session` | How to build a multi-party app session | +| `migrate-sdk` | How to migrate from SDK v0.5.3 to v1.x | +| `build-ai-agent` | How to build an autonomous payment agent | + +Published via: `npx skills add yellow-org/nitrolite-skills` + +--- + +## Phase 4: Hosted Remote MCP (Future) + +Deploy the MCP server as a hosted HTTP endpoint so developers don't need to install anything at all: + +```json +{ + "mcpServers": { + "nitrolite": { + "url": "https://mcp.yellow.org/ts" + } + } +} +``` + +Uses MCP Streamable HTTP transport. Requires infrastructure (Cloudflare Workers or similar) and API key management. Only Thirdweb does this in Web3 today. + +--- + +## Summary: Developer Experience by Phase + +| Phase | Developer experience | Effort | +|-------|---------------------|--------| +| Today | Clone repo, run from source | Already done | +| Phase 1 (npm publish) | `npx @yellow-org/sdk-mcp` — covers both TS and Go SDKs | Medium | +| Phase 2 (context files) | IDE auto-loads project rules | Low | +| Phase 3 (skills) | `npx skills add yellow-org/nitrolite-skills` | Medium | +| Phase 4 (hosted) | Add a URL to IDE config | High | + +**Priority order:** Phase 1 > Phase 2 > Phase 3 > Phase 4 + +Phase 1 (npm publish) is the single highest-impact step. It's the difference between "only our team can use this" and "any developer on the internet can use this with one command." diff --git a/sdk/go/CLAUDE.md b/sdk/go/CLAUDE.md new file mode 100644 index 000000000..254b4ccfe --- /dev/null +++ b/sdk/go/CLAUDE.md @@ -0,0 +1,42 @@ +# Go SDK + +Official Go SDK for building backend integrations and CLI tools on Nitrolite state channels. + +## Quick Reference + +```bash +# All commands run from the REPO ROOT (not sdk/go/) +go test ./sdk/go/... -v # Test SDK only +go build ./sdk/go/... # Build SDK only +go vet ./sdk/go/... # Lint SDK only + +go test ./... # Test EVERYTHING (clearnode, pkg, sdk, cerebro) +``` + +**Important:** This is NOT a separate Go module. It shares the root `go.mod` (`github.com/layer-3/nitrolite`, Go 1.25). + +## Source Layout + +| File | Purpose | +|------|---------| +| `client.go` | Main SDK client — entry point for all operations | +| `config.go` | Functional options pattern for client configuration | +| `channel.go` | Channel operations (open, close, resize, challenge) | +| `utils.go` | Encoding/packing helpers | +| `doc.go` | Package-level documentation with usage examples | +| `*_test.go` | Tests (same directory, standard Go convention) | +| `mock_dialer_test.go` | Test doubles for WebSocket dialer | + +## Architecture + +- Uses **functional options pattern** for configuration (see `config.go`) +- Key dependency: `github.com/ethereum/go-ethereum` for Ethereum interactions +- Shared packages in `pkg/` (sign, core, rpc, app, blockchain, log) are used by both this SDK and `clearnode/` +- WebSocket-based communication with clearnode via JSON-RPC + +## Conventions + +- Standard Go: `gofmt`, exported names have doc comments +- Error handling: always check and return errors, never ignore +- Tests use standard `testing` package with `*_test.go` in same directory +- Check `pkg/` for shared utilities before creating new ones diff --git a/sdk/mcp/.gitignore b/sdk/mcp/.gitignore new file mode 100644 index 000000000..b94707787 --- /dev/null +++ b/sdk/mcp/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/sdk/mcp/README.md b/sdk/mcp/README.md new file mode 100644 index 000000000..f8ed4935d --- /dev/null +++ b/sdk/mcp/README.md @@ -0,0 +1,122 @@ +# Nitrolite MCP Server (Unified) + +An MCP (Model Context Protocol) server that exposes the **Nitrolite SDK** knowledge base to AI coding tools — covering both the **TypeScript SDK** (`@yellow-org/sdk`) and the **Go SDK** (`github.com/layer-3/nitrolite/sdk/go`). It reads SDK source code, protocol documentation, and Go type definitions at startup, making every method, type, enum, and protocol concept discoverable by AI agents. + +## Quick Start + +```bash +# From repo root: +cd sdk/mcp && npm install && cd ../.. +``` + +Add to `.mcp.json` (already configured in this repo): + +```json +{ + "mcpServers": { + "nitrolite": { + "command": "npm", + "args": ["--prefix", "sdk/mcp", "exec", "--", "tsx", "sdk/mcp/src/index.ts"] + } + } +} +``` + +Any MCP-compatible tool (Claude Code, Cursor, Windsurf, VS Code Copilot) auto-discovers the server from `.mcp.json`. + +## What's Inside + +- **30 resources** — API reference (TS + Go), protocol docs, security patterns, examples (TS + Go), use cases, migration +- **8 tools** — method lookup, type lookup, search, RPC format, import validation, concept explanation, scaffolding (TS + Go) +- **3 prompts** — guided workflows for building apps (covers both TS and Go), migrating from v0.5.3, building AI agents + +--- + +## Tools + +### `lookup_method` + +Look up a Client method by name. Supports an optional `language` parameter. + +```text +> lookup_method({ name: "transfer" }) # TypeScript (default) +> lookup_method({ name: "Transfer", language: "go" }) # Go SDK +> lookup_method({ name: "transfer", language: "both" }) # Both SDKs +``` + +### `lookup_type` + +Look up a type, interface, struct, or enum by name. + +```text +> lookup_type({ name: "AppDefinitionV1" }) # TypeScript (default) +> lookup_type({ name: "AppSessionV1", language: "go" }) # Go (from pkg/app) +> lookup_type({ name: "ChannelStatus", language: "go" }) # Go enum with values +``` + +### `search_api` + +Fuzzy search across methods and types. + +```text +> search_api({ query: "transfer" }) # TypeScript (default) +> search_api({ query: "session", language: "go" }) # Go methods + types +> search_api({ query: "channel", language: "both" }) # Both SDKs +``` + +### `scaffold_project` + +Generate a starter project. TypeScript templates output `package.json` + `tsconfig.json` + `src/index.ts`. Go templates output `go.mod` + `main.go`. + +| Template | Language | Description | +|----------|----------|-------------| +| `transfer-app` | TypeScript | Deposit, transfer, close channel | +| `app-session` | TypeScript | Multi-party app session | +| `ai-agent` | TypeScript | Autonomous payment agent | +| `go-transfer-app` | Go | Deposit, transfer, close channel | +| `go-app-session` | Go | Multi-party app session | +| `go-ai-agent` | Go | Autonomous payment agent with graceful shutdown | + +### Other Tools + +- `get_rpc_method` — 0.5.x compat method to v1 wire format +- `validate_import` — check if a symbol is in `@yellow-org/sdk-compat` or `@yellow-org/sdk` +- `explain_concept` — plain-English explanation of protocol concepts +- `lookup_rpc_method` — full v1 RPC method lookup from `docs/api.yaml` + +--- + +## Resources + +### TypeScript SDK +- `nitrolite://api/methods` — TS client methods by category +- `nitrolite://api/types` — TS interfaces and type aliases +- `nitrolite://api/enums` — TS enums + +### Go SDK +- `nitrolite://go-api/methods` — Go client methods by category +- `nitrolite://go-api/types` — Go structs and enum types (from `pkg/` and `sdk/go/`) + +### Examples +- `nitrolite://examples/full-transfer-script` — complete TypeScript transfer script +- `nitrolite://examples/full-app-session-script` — complete TypeScript app session script +- `nitrolite://go-examples/full-transfer-script` — complete Go transfer script +- `nitrolite://go-examples/full-app-session-script` — complete Go app session script + +### Protocol & Security +- `nitrolite://protocol/{overview,terminology,cryptography,wire-format,rpc-methods,auth-flow,channel-lifecycle,state-model,enforcement,cross-chain,interactions}` +- `nitrolite://security/{overview,app-session-patterns,state-invariants}` + +### Use Cases +- `nitrolite://use-cases`, `nitrolite://use-cases/ai-agents` + +### Migration +- `nitrolite://migration/overview` + +--- + +## Prompts + +- `create-channel-app` — step-by-step guide covering both TypeScript and Go SDKs +- `migrate-from-v053` — migration guide from `@layer-3/nitrolite` v0.5.3 to the compat layer +- `build-ai-agent-app` — AI agent payments guide for both TypeScript and Go diff --git a/sdk/mcp/package-lock.json b/sdk/mcp/package-lock.json new file mode 100644 index 000000000..5ba80c448 --- /dev/null +++ b/sdk/mcp/package-lock.json @@ -0,0 +1,1724 @@ +{ + "name": "@yellow-org/sdk-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@yellow-org/sdk-mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.28.0", + "zod": "^3.23.0" + }, + "bin": { + "nitrolite-sdk-mcp": "src/index.ts" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.12.tgz", + "integrity": "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz", + "integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.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": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "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": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "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": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "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.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "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": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "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": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "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/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/sdk/mcp/package.json b/sdk/mcp/package.json new file mode 100644 index 000000000..f8a09bc55 --- /dev/null +++ b/sdk/mcp/package.json @@ -0,0 +1,28 @@ +{ + "name": "@yellow-org/sdk-mcp", + "version": "0.1.0", + "description": "Unified MCP server for Nitrolite — TypeScript and Go SDK context for AI agents and IDEs", + "type": "module", + "main": "src/index.ts", + "bin": { + "nitrolite-sdk-mcp": "src/index.ts" + }, + "scripts": { + "start": "tsx src/index.ts", + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.28.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "license": "MIT" +} diff --git a/sdk/mcp/src/index.ts b/sdk/mcp/src/index.ts new file mode 100644 index 000000000..1f493c29f --- /dev/null +++ b/sdk/mcp/src/index.ts @@ -0,0 +1,2312 @@ +#!/usr/bin/env node +/** + * Nitrolite SDK MCP Server + * + * Exposes the Nitrolite SDK API surface to AI agents and IDEs via the + * Model Context Protocol. Reads SDK source at startup to build structured + * knowledge of methods, types, enums, and examples. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { readFileSync, existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SDK_ROOT = resolve(__dirname, '../../ts'); +const COMPAT_ROOT = resolve(__dirname, '../../ts-compat'); +const REPO_ROOT = resolve(__dirname, '../../..'); +const PROTOCOL_DOCS = resolve(REPO_ROOT, 'docs/protocol'); +const API_YAML = resolve(REPO_ROOT, 'docs/api.yaml'); +const GO_SDK_ROOT = resolve(REPO_ROOT, 'sdk/go'); +const PKG_ROOT = resolve(REPO_ROOT, 'pkg'); +const GO_MODULE_PATH = 'github.com/layer-3/nitrolite'; +const GO_MODULE_VERSION = 'v1.2.0'; + +// --------------------------------------------------------------------------- +// Helpers — read SDK sources at startup +// --------------------------------------------------------------------------- + +function readFile(path: string): string { + if (!existsSync(path)) return ''; + return readFileSync(path, 'utf-8'); +} + +/** Extract named exports from a barrel file */ +function extractExports(content: string): string[] { + const names: string[] = []; + // Match: export { Foo, type Bar } from '...' and export type { Baz } from '...' + for (const m of content.matchAll(/export\s+(type\s+)?\{([^}]+)\}/g)) { + const groupIsTypeOnly = Boolean(m[1]); + for (const item of m[2].split(',')) { + const raw = item.trim(); + if (!raw || raw.startsWith('//')) continue; + + const itemIsTypeOnly = groupIsTypeOnly || raw.startsWith('type '); + const spec = raw.replace(/^type\s+/, '').trim(); + const aliasMatch = spec.match(/^(\w+)(?:\s+as\s+(\w+))?$/); + if (!aliasMatch) continue; + + const exportedName = aliasMatch[2] || aliasMatch[1]; + names.push(itemIsTypeOnly ? `type ${exportedName}` : exportedName); + } + } + // Match: export * from '...' + for (const m of content.matchAll(/export\s+\*\s+from\s+'([^']+)'/g)) { + names.push(`* from '${m[1]}'`); + } + return names; +} + +function findNamedExport(exports: string[], symbol: string): { found: boolean; isTypeOnly: boolean } { + if (exports.includes(`type ${symbol}`)) return { found: true, isTypeOnly: true }; + if (exports.includes(symbol)) return { found: true, isTypeOnly: false }; + return { found: false, isTypeOnly: false }; +} + +function renderImportStatement(pkg: string, symbol: string, isTypeOnly: boolean): string { + return isTypeOnly + ? `import type { ${symbol} } from '${pkg}';` + : `import { ${symbol} } from '${pkg}';`; +} + +// --------------------------------------------------------------------------- +// SDK Data — populated at startup +// --------------------------------------------------------------------------- + +interface MethodInfo { + name: string; + signature: string; + description: string; + category: string; +} + +interface TypeInfo { + name: string; + kind: 'interface' | 'type' | 'enum' | 'class'; + fields: string; + source: string; +} + +const methods: MethodInfo[] = []; +const types: TypeInfo[] = []; +const compatExports: string[] = []; + +// Go SDK data — populated at startup +interface GoTypeInfo { + name: string; + kind: 'struct' | 'enum' | 'type'; + fields: string; + source: string; +} + +interface GoMethodInfo { + name: string; + signature: string; + comment: string; + category: string; +} + +const goTypes: GoTypeInfo[] = []; +const goMethods: GoMethodInfo[] = []; + +// Protocol docs loaded at startup +const protocolDocs: Record = {}; + +// Terminology concept → definition map +const concepts: Map = new Map(); + +// V1 RPC method → { description, request fields, response fields } map (from docs/api.yaml) +interface RPCMethodDoc { + /** Fully qualified v1 method name, e.g. "channels.v1.get_home_channel" */ + method: string; + /** API group, e.g. "channels" */ + group: string; + description: string; + requestFields: string; + responseFields: string; +} +const rpcMethodDocs: Map = new Map(); + +function loadClientMethods(): void { + const content = readFile(resolve(SDK_ROOT, 'src/client.ts')); + const re = /\/\*\*\s*([\s\S]*?)\*\/\s*(?:(public|protected|private)\s+)?(?:static\s+)?(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*Promise<([^>]+)>|\s*:\s*(\S+))?/g; + let match; + while ((match = re.exec(content)) !== null) { + const doc = match[1].replace(/\s*\*\s*/g, ' ').trim(); + const visibility = match[2] ?? 'public'; + const name = match[3]; + const params = match[4].trim(); + const returnType = match[5] || match[6] || 'void'; + + // Index only public client methods. + if (visibility !== 'public' || name.startsWith('_')) continue; + + const category = categorizeMethod(name); + const isAsync = /\basync\b/.test(match[0]); + const returnStr = isAsync ? `Promise<${returnType}>` : returnType; + + methods.push({ + name, + signature: `${name}(${params}): ${returnStr}`, + description: doc || `SDK method: ${name}`, + category, + }); + } +} + +function categorizeMethod(name: string): string { + if (/channel/i.test(name)) return 'Channels'; + if (/deposit|withdraw|transfer|escrow|approve/i.test(name)) return 'Transactions'; + if (/app.*session|submitApp/i.test(name)) return 'App Sessions'; + if (/sign|signer|key/i.test(name)) return 'Signing'; + if (/ping|config|asset|balance|blockchain/i.test(name)) return 'Node & Queries'; + return 'Other'; +} + +function categorizeGoMethod(name: string): string { + const lower = name.toLowerCase(); + if (/channel|deposit|withdraw|transfer|checkpoint|challenge|acknowledge|close/.test(lower)) return 'Channels & Transactions'; + if (/appsession|appstate|appdef|rebalance/.test(lower)) return 'App Sessions'; + if (/sessionkey|keystate/.test(lower)) return 'Session Keys'; + if (/escrow|security|locked/.test(lower)) return 'Security Tokens'; + if (/app/.test(lower) && /register/.test(lower)) return 'App Registry'; + if (/balance|transaction|allowance|user/.test(lower)) return 'User Queries'; + if (/config|blockchain|asset|ping/.test(lower)) return 'Node & Config'; + return 'Other'; +} + +function loadGoTypes(): void { + const fileSets = [ + { path: resolve(PKG_ROOT, 'core/types.go'), source: 'pkg/core' }, + { path: resolve(PKG_ROOT, 'app/app_session_v1.go'), source: 'pkg/app' }, + { path: resolve(PKG_ROOT, 'rpc/types.go'), source: 'pkg/rpc' }, + { path: resolve(GO_SDK_ROOT, 'config.go'), source: 'sdk/go' }, + { path: resolve(GO_SDK_ROOT, 'app_session.go'), source: 'sdk/go' }, + { path: resolve(GO_SDK_ROOT, 'app_registry.go'), source: 'sdk/go' }, + { path: resolve(GO_SDK_ROOT, 'user.go'), source: 'sdk/go' }, + { path: resolve(GO_SDK_ROOT, 'channel.go'), source: 'sdk/go' }, + ]; + + for (const { path, source } of fileSets) { + const content = readFile(path); + if (!content) continue; + + // Pass 1: collect enum-like type declarations (name → kind) + const enumTypeMap = new Map(); + for (const m of content.matchAll(/type\s+([A-Z]\w+)\s+(uint\d+|int\d+|string)\s*\n/g)) { + const info: GoTypeInfo = { name: m[1], kind: 'enum', fields: '', source }; + enumTypeMap.set(m[1], info); + } + + // Pass 2: parse const(...) and var(...) blocks to collect enum values + for (const blockMatch of content.matchAll(/(?:const|var)\s*\(([^)]+)\)/gs)) { + const block = blockMatch[1]; + let currentType: string | undefined; + + for (const rawLine of block.split('\n')) { + const line = rawLine.replace(/\/\/.*$/, '').trim(); + if (!line) continue; // blank lines don't reset currentType + + // Fully-annotated: ExportedName TypeName = ... or ExportedName TypeName + const fullAnnotated = line.match(/^([A-Z]\w+)\s+([A-Z]\w+)\s*(?:=.*)?$/); + if (fullAnnotated) { + const [, valueName, typeName] = fullAnnotated; + if (enumTypeMap.has(typeName)) { + currentType = typeName; + const info = enumTypeMap.get(typeName)!; + info.fields += (info.fields ? '\n' : '') + valueName; + continue; + } + } + + // Untyped-assignment: ExportedName = value (no type annotation) + const untypedAssign = line.match(/^([A-Z]\w+)\s*=\s*(.+)$/); + if (untypedAssign) { + const [, valueName] = untypedAssign; + let resolvedType = currentType; + if (!resolvedType) { + // Prefix inference: find enum type whose name is a prefix of valueName + for (const typeName of enumTypeMap.keys()) { + if (valueName.startsWith(typeName) && valueName[typeName.length]?.match(/[A-Z_]/)) { + resolvedType = typeName; + break; + } + } + } + if (resolvedType && enumTypeMap.has(resolvedType)) { + currentType = resolvedType; + const info = enumTypeMap.get(resolvedType)!; + info.fields += (info.fields ? '\n' : '') + valueName; + } + continue; + } + + // Bare identifier: ExportedName (iota follow-on) + const bareIdent = line.match(/^([A-Z]\w+)\s*$/); + if (bareIdent && currentType && enumTypeMap.has(currentType)) { + const info = enumTypeMap.get(currentType)!; + info.fields += (info.fields ? '\n' : '') + bareIdent[1]; + continue; + } + + // Non-exported or unrecognised → reset + if (!line.match(/^[A-Z]/)) currentType = undefined; + } + } + for (const info of enumTypeMap.values()) { + if (info.fields) goTypes.push(info); + } + + // Structs + for (const m of content.matchAll(/(?:\/\/[^\n]*\n)*type\s+([A-Z]\w+)\s+struct\s*\{([^}]*)\}/gs)) { + goTypes.push({ name: m[1], kind: 'struct', fields: m[2].trim(), source }); + } + + // Functional option types (e.g. type Option func(*Config)) + for (const m of content.matchAll(/type\s+([A-Z]\w+)\s+(func\([^)]*\)[^\n]*)/g)) { + goTypes.push({ name: m[1], kind: 'type', fields: m[2].trim(), source }); + } + } +} + +function loadGoSdkMethods(): void { + // Prepend NewClient constructor (no receiver) + goMethods.push({ + name: 'NewClient', + signature: 'func NewClient(wsURL string, stateSigner core.ChannelSigner, rawSigner sign.Signer, opts ...Option) (*Client, error)', + comment: 'Creates a new Nitrolite SDK client connected to a clearnode', + category: 'Connection', + }); + + const files = [ + { path: resolve(GO_SDK_ROOT, 'channel.go'), category: '' }, + { path: resolve(GO_SDK_ROOT, 'node.go'), category: 'Node & Config' }, + { path: resolve(GO_SDK_ROOT, 'user.go'), category: 'User Queries' }, + { path: resolve(GO_SDK_ROOT, 'app_session.go'), category: 'App Sessions' }, + { path: resolve(GO_SDK_ROOT, 'app_registry.go'), category: 'App Registry' }, + { path: resolve(GO_SDK_ROOT, 'client.go'), category: '' }, + ]; + + const methodRe = /((?:\/\/[^\n]*\n)+)func \(c \*Client\) (\w+)\(([^)]*)\)\s*(.*)/g; + + for (const { path, category } of files) { + const content = readFile(path); + if (!content) continue; + methodRe.lastIndex = 0; + let match; + while ((match = methodRe.exec(content)) !== null) { + const rawComment = match[1].trim(); + const comment = rawComment.split('\n') + .map(l => l.replace(/^\/\/ ?/, '').trim()) + .filter(Boolean) + .join(' '); + const name = match[2]; + const params = match[3]; + const returns = match[4].trim(); + if (name[0] >= 'a' && name[0] <= 'z') continue; // skip unexported + const cat = category || categorizeGoMethod(name); + goMethods.push({ + name, + signature: `func (c *Client) ${name}(${params}) ${returns}`, + comment, + category: cat, + }); + } + } +} + +function buildGoApiMethodsContent(): string { + const ORDER = ['Connection', 'Channels & Transactions', 'App Sessions', 'Session Keys', 'Security Tokens', 'App Registry', 'User Queries', 'Node & Config', 'Other']; + const grouped = new Map(); + for (const m of goMethods) { + const arr = grouped.get(m.category) ?? []; + arr.push(m); + grouped.set(m.category, arr); + } + let text = '# Nitrolite Go SDK — Client Methods\n\nPackage: `github.com/layer-3/nitrolite/sdk/go`\n\n'; + for (const cat of ORDER) { + const ms = grouped.get(cat); + if (!ms?.length) continue; + text += `## ${cat}\n\n`; + for (const m of ms) { + text += `### \`${m.name}\`\n\`\`\`go\n${m.signature}\n\`\`\`\n${m.comment}\n\n`; + } + } + return text; +} + +function buildGoTypesContent(): string { + const bySource = new Map(); + for (const t of goTypes) { + const arr = bySource.get(t.source) ?? []; + arr.push(t); + bySource.set(t.source, arr); + } + let text = '# Nitrolite Go SDK — Types\n\n'; + for (const [src, ts] of [...bySource.entries()].sort()) { + text += `## ${src}\n\n`; + for (const t of ts) { + if (t.kind === 'struct') { + text += `### \`${t.name}\` (struct)\n\`\`\`go\ntype ${t.name} struct {\n${t.fields}\n}\n\`\`\`\n\n`; + } else if (t.kind === 'enum') { + text += `### \`${t.name}\` (enum)\n**Values:**\n${t.fields.split('\n').map(v => `- \`${v}\``).join('\n')}\n\n`; + } else { + text += `### \`${t.name}\` (${t.kind})\n\`\`\`go\ntype ${t.name} ${t.fields}\n\`\`\`\n\n`; + } + } + } + return text; +} + +function loadTypes(): void { + const files = [ + { path: resolve(SDK_ROOT, 'src/core/types.ts'), source: 'sdk/ts (core)' }, + { path: resolve(SDK_ROOT, 'src/rpc/types.ts'), source: 'sdk/ts (rpc)' }, + { path: resolve(SDK_ROOT, 'src/app/types.ts'), source: 'sdk/ts (app)' }, + { path: resolve(COMPAT_ROOT, 'src/types.ts'), source: 'sdk-compat' }, + ]; + + for (const { path, source } of files) { + const content = readFile(path); + if (!content) continue; + + // Enums + for (const m of content.matchAll(/export\s+enum\s+(\w+)\s*\{([^}]+)\}/g)) { + types.push({ name: m[1], kind: 'enum', fields: m[2].trim(), source }); + } + // Interfaces + for (const m of content.matchAll(/export\s+interface\s+(\w+)(?:\s+extends\s+\w+)?\s*\{([^}]+)\}/g)) { + types.push({ name: m[1], kind: 'interface', fields: m[2].trim(), source }); + } + // Type aliases + for (const m of content.matchAll(/export\s+type\s+(\w+)\s*=\s*([^;]+);/g)) { + types.push({ name: m[1], kind: 'type', fields: m[2].trim(), source }); + } + } +} + +function loadCompatExports(): void { + const content = readFile(resolve(COMPAT_ROOT, 'src/index.ts')); + compatExports.push(...extractExports(content)); +} + +function loadProtocolDocs(): void { + const files = [ + 'overview.md', 'terminology.md', 'cryptography.md', 'state-model.md', + 'channel-protocol.md', 'enforcement.md', 'cross-chain-and-assets.md', + 'interactions.md', 'security-and-limitations.md', + ]; + for (const file of files) { + const key = file.replace('.md', ''); + const content = readFile(resolve(PROTOCOL_DOCS, file)); + if (content) protocolDocs[key] = content; + } +} + +function loadTerminology(): void { + const content = protocolDocs['terminology'] || ''; + // Parse ### headings and their following paragraphs + const sections = content.split(/^### /m).slice(1); + for (const section of sections) { + const lines = section.trim().split('\n'); + const name = lines[0].trim(); + const body = lines.slice(1).join('\n').trim() + .replace(/^[\s\n]+/, '') + .split(/\n(?=### |## )/)[0] + .trim(); + if (name && body) { + concepts.set(name.toLowerCase(), `**${name}**\n\n${body}`); + } + } +} + +function loadV1API(): void { + const content = readFile(API_YAML); + if (!content) return; + + // Simple line-based parser for the well-structured api.yaml + // Extracts: group name, method name, description, request fields, response fields + let currentGroup = ''; + let currentMethod = ''; + let currentDesc = ''; + let currentSection: 'none' | 'request' | 'response' = 'none'; + let requestFields: string[] = []; + let responseFields: string[] = []; + + const flushMethod = () => { + if (currentGroup && currentMethod) { + const fqn = `${currentGroup}.v1.${currentMethod}`; + rpcMethodDocs.set(fqn, { + method: fqn, + group: currentGroup, + description: currentDesc, + requestFields: requestFields.length > 0 ? requestFields.join(', ') : '(none)', + responseFields: responseFields.length > 0 ? responseFields.join(', ') : '(none)', + }); + } + currentMethod = ''; + currentDesc = ''; + currentSection = 'none'; + requestFields = []; + responseFields = []; + }; + + // Only parse the api: section + const apiStart = content.indexOf('\napi:\n'); + if (apiStart === -1) return; + const lines = content.slice(apiStart).split('\n'); + + for (const line of lines) { + // Group: " - name: channels" + const groupMatch = line.match(/^ {4}- name:\s+(.+)/); + if (groupMatch) { + flushMethod(); + currentGroup = groupMatch[1].trim(); + continue; + } + + // Method: " - name: get_home_channel" + const methodMatch = line.match(/^ {12}- name:\s+(.+)/); + if (methodMatch) { + flushMethod(); + currentMethod = methodMatch[1].trim(); + continue; + } + + // Method description: " description: ..." + if (currentMethod && !currentDesc) { + const descMatch = line.match(/^ {14}description:\s+(.+)/); + if (descMatch) { + currentDesc = descMatch[1].trim(); + continue; + } + } + + // Request/response section markers + if (currentMethod) { + const sectionMatch = line.match(/^ {14}(request|response):/); + if (sectionMatch) { + currentSection = sectionMatch[1] as 'request' | 'response'; + continue; + } + + // Field name within request/response: " - field_name: wallet" + const fieldMatch = line.match(/^ {16}- field_name:\s+(.+)/); + if (fieldMatch) { + if (currentSection === 'request') requestFields.push(fieldMatch[1].trim()); + else if (currentSection === 'response') responseFields.push(fieldMatch[1].trim()); + continue; + } + + // errors: or events: sections end request/response + if (/^ {14}(errors|events):/.test(line)) { + currentSection = 'none'; + } + } + } + flushMethod(); // flush last method +} + +// --------------------------------------------------------------------------- +// Go SDK static content constants ported from the former Go MCP server. +// --------------------------------------------------------------------------- + +const AUTH_FLOW_CONTENT = `# Request Signing & Authorization + +In v1, every RPC request includes a \`sig\` field — the client's signature over the entire \`req\` tuple. This is the authorization mechanism. There is no separate authentication handshake; request signatures are the identity proof. + +## Session Keys + +Session keys enable delegated signing with scoped permissions. They are managed via: +- \`channels.v1.submit_session_key_state\` — register/update channel session keys +- \`app_sessions.v1.submit_session_key_state\` — register/update app session keys + +Session keys have: +- Per-asset allowances with spending caps +- Expiration timestamps +- Scoping to specific applications and app sessions + +## Wire Format + +\`\`\`json +// Every request is signed +{ "req": [REQUEST_ID, "channels.v1.submit_state", { ... }, TIMESTAMP], "sig": ["0xClientSignature..."] } + +// Server responds with its own signature +{ "res": [REQUEST_ID, "channels.v1.submit_state", { ... }, TIMESTAMP], "sig": ["0xServerSignature..."] } +\`\`\` + +## Note on 0.5.x Compat + +The \`@yellow-org/sdk-compat\` layer exposes legacy auth helpers (\`createAuthRequestMessage\`, \`createAuthVerifyMessage\`) that implement a challenge-response flow with JWT. This is the 0.5.x auth surface bridged to v1. New applications using \`@yellow-org/sdk\` directly do not use this flow. +`; + +const GO_TRANSFER_EXAMPLE = `# Complete Go Transfer Script + +\`\`\`go +package main + +import ( + "context" + "log" + "os" + "time" + + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" + "github.com/shopspring/decimal" +) + +func main() { + privateKey := os.Getenv("PRIVATE_KEY") + clearnodeURL := os.Getenv("CLEARNODE_URL") + rpcURL := os.Getenv("RPC_URL") + recipient := os.Getenv("RECIPIENT") + var chainID uint64 = 11155111 // Sepolia + + stateSigner, err := sign.NewEthereumMsgSigner(privateKey) + if err != nil { log.Fatal(err) } + txSigner, err := sign.NewEthereumRawSigner(privateKey) + if err != nil { log.Fatal(err) } + + client, err := sdk.NewClient(clearnodeURL, stateSigner, txSigner, + sdk.WithBlockchainRPC(chainID, rpcURL), + ) + if err != nil { log.Fatal(err) } + defer client.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + // Approve token spending (one-time) + _, err = client.ApproveToken(ctx, chainID, "usdc", decimal.NewFromInt(1000)) + if err != nil { log.Fatal(err) } + + // Deposit — creates channel if needed + state, err := client.Deposit(ctx, chainID, "usdc", decimal.NewFromInt(10)) + if err != nil { log.Fatal(err) } + log.Printf("Deposited 10 USDC, state version: %d", state.Version) + + // Checkpoint on-chain + txHash, err := client.Checkpoint(ctx, "usdc") + if err != nil { log.Fatal(err) } + log.Printf("On-chain tx: %s", txHash) + + // Transfer + _, err = client.Transfer(ctx, recipient, "usdc", decimal.NewFromInt(5)) + if err != nil { log.Fatal(err) } + log.Println("Transferred 5 USDC") + + // Close channel — prepare finalize state, then checkpoint + _, err = client.CloseHomeChannel(ctx, "usdc") + if err != nil { log.Fatal(err) } + closeTx, err := client.Checkpoint(ctx, "usdc") + if err != nil { log.Fatal(err) } + log.Printf("Channel closed, tx: %s", closeTx) +} +\`\`\` + +## Environment Variables + +- \`PRIVATE_KEY\` — Hex private key (without 0x prefix) +- \`CLEARNODE_URL\` — WebSocket URL +- \`RPC_URL\` — Ethereum RPC endpoint +- \`RECIPIENT\` — Recipient address +`; + +const GO_APP_SESSION_EXAMPLE = `# Complete Go App Session Script + +\`\`\`go +package main + +import ( + "context" + "log" + "os" + "strconv" + "time" + + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" + "github.com/shopspring/decimal" +) + +func main() { + privateKey := os.Getenv("PRIVATE_KEY") + clearnodeURL := os.Getenv("CLEARNODE_URL") + rpcURL := os.Getenv("RPC_URL") + peerAddr := os.Getenv("PEER_ADDRESS") + var chainID uint64 = 11155111 + + stateSigner, err := sign.NewEthereumMsgSigner(privateKey) + if err != nil { log.Fatal(err) } + txSigner, err := sign.NewEthereumRawSigner(privateKey) + if err != nil { log.Fatal(err) } + + client, err := sdk.NewClient(clearnodeURL, stateSigner, txSigner, + sdk.WithBlockchainRPC(chainID, rpcURL), + ) + if err != nil { log.Fatal(err) } + defer client.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + myAddr := client.GetUserAddress() + + // Fund the home channel before moving funds into the app session + if _, err = client.Deposit(ctx, chainID, "usdc", decimal.NewFromInt(20)); err != nil { log.Fatal(err) } + txHash, err := client.Checkpoint(ctx, "usdc") + if err != nil { log.Fatal(err) } + log.Printf("Home channel funded with 20 USDC, tx: %s", txHash) + + // 1. Create app session + definition := app.AppDefinitionV1{ + ApplicationID: "my-game", + Participants: []app.AppParticipantV1{ + {WalletAddress: myAddr, SignatureWeight: 50}, + {WalletAddress: peerAddr, SignatureWeight: 50}, + }, + Quorum: 100, + Nonce: 1, + } + + quorumSigs := []string{"0xMySignature...", "0xPeerSignature..."} + sessionID, versionStr, status, err := client.CreateAppSession(ctx, definition, "{}", quorumSigs) + if err != nil { log.Fatal(err) } + log.Printf("Session %s created (version: %s, status: %s)", sessionID, versionStr, status) + + initVersion, err := strconv.ParseUint(versionStr, 10, 64) + if err != nil { log.Fatal(err) } + + // 2. Fund the app session before submitting non-zero allocations + depositUpdate := app.AppStateUpdateV1{ + AppSessionID: sessionID, + Intent: app.AppStateUpdateIntentDeposit, + Version: initVersion + 1, + Allocations: []app.AppAllocationV1{ + {Participant: myAddr, Asset: "usdc", Amount: decimal.NewFromInt(15)}, + {Participant: peerAddr, Asset: "usdc", Amount: decimal.NewFromInt(5)}, + }, + SessionData: "{}", + } + if _, err = client.SubmitAppSessionDeposit(ctx, depositUpdate, quorumSigs, "usdc", decimal.NewFromInt(20)); err != nil { log.Fatal(err) } + log.Println("Session funded with 20 USDC") + + // 3. Submit state update (version = initial + 2) + update := app.AppStateUpdateV1{ + AppSessionID: sessionID, + Intent: app.AppStateUpdateIntentOperate, + Version: initVersion + 2, + Allocations: depositUpdate.Allocations, + SessionData: "{\"round\":1,\"winner\":\"me\"}", + } + operateSigs := []string{"0xMySig...", "0xPeerSig..."} + if err = client.SubmitAppState(ctx, update, operateSigs); err != nil { log.Fatal(err) } + log.Println("State updated") + + // 4. Close session — submit with Close intent (version = initial + 3) + closeUpdate := update + closeUpdate.Intent = app.AppStateUpdateIntentClose + closeUpdate.Version = initVersion + 3 + closeSigs := []string{"0xMyCloseSig...", "0xPeerCloseSig..."} + if err = client.SubmitAppState(ctx, closeUpdate, closeSigs); err != nil { log.Fatal(err) } + log.Println("Session closed") +} +\`\`\` +`; + +const GO_SCAFFOLD_TRANSFER = `package main + +import ( +\t"context" +\t"log" +\t"os" +\t"time" + +\t"github.com/layer-3/nitrolite/pkg/sign" +\tsdk "github.com/layer-3/nitrolite/sdk/go" +\t"github.com/shopspring/decimal" +) + +func main() { +\tstateSigner, err := sign.NewEthereumMsgSigner(os.Getenv("PRIVATE_KEY")) +\tif err != nil { log.Fatal(err) } +\ttxSigner, err := sign.NewEthereumRawSigner(os.Getenv("PRIVATE_KEY")) +\tif err != nil { log.Fatal(err) } + +\tclient, err := sdk.NewClient(os.Getenv("CLEARNODE_URL"), stateSigner, txSigner, +\t\tsdk.WithBlockchainRPC(11155111, os.Getenv("RPC_URL")), +\t) +\tif err != nil { log.Fatal(err) } +\tdefer client.Close() + +\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) +\tdefer cancel() + +\t_, err = client.Deposit(ctx, 11155111, "usdc", decimal.NewFromInt(10)) +\tif err != nil { log.Fatal(err) } +\ttxHash, err := client.Checkpoint(ctx, "usdc") +\tif err != nil { log.Fatal(err) } +\tlog.Printf("Deposited 10 USDC, tx: %s", txHash) + +\t_, err = client.Transfer(ctx, os.Getenv("RECIPIENT"), "usdc", decimal.NewFromInt(5)) +\tif err != nil { log.Fatal(err) } +\tlog.Println("Transferred 5 USDC") +} +`; + +const GO_SCAFFOLD_APP_SESSION = `package main + +import ( +\t"context" +\t"log" +\t"os" +\t"strconv" +\t"time" + +\t"github.com/layer-3/nitrolite/pkg/app" +\t"github.com/layer-3/nitrolite/pkg/sign" +\tsdk "github.com/layer-3/nitrolite/sdk/go" +\t"github.com/shopspring/decimal" +) + +func main() { +\tstateSigner, err := sign.NewEthereumMsgSigner(os.Getenv("PRIVATE_KEY")) +\tif err != nil { log.Fatal(err) } +\ttxSigner, err := sign.NewEthereumRawSigner(os.Getenv("PRIVATE_KEY")) +\tif err != nil { log.Fatal(err) } + +\tclient, err := sdk.NewClient(os.Getenv("CLEARNODE_URL"), stateSigner, txSigner, +\t\tsdk.WithBlockchainRPC(11155111, os.Getenv("RPC_URL")), +\t) +\tif err != nil { log.Fatal(err) } +\tdefer client.Close() + +\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) +\tdefer cancel() + +\tmyAddr := client.GetUserAddress() +\tpeer := os.Getenv("PEER_ADDRESS") + +\t// Fund the home channel before moving funds into the app session +\tif _, err = client.Deposit(ctx, 11155111, "usdc", decimal.NewFromInt(20)); err != nil { log.Fatal(err) } +\ttxHash, err := client.Checkpoint(ctx, "usdc") +\tif err != nil { log.Fatal(err) } +\tlog.Printf("Home channel funded with 20 USDC, tx: %s", txHash) + +\tdef := app.AppDefinitionV1{ +\t\tApplicationID: "my-app", +\t\tParticipants: []app.AppParticipantV1{ +\t\t\t{WalletAddress: myAddr, SignatureWeight: 50}, +\t\t\t{WalletAddress: peer, SignatureWeight: 50}, +\t\t}, +\t\tQuorum: 100, +\t\tNonce: 1, +\t} + +\tquorumSigs := []string{"0xMySig...", "0xPeerSig..."} +\tsessionID, versionStr, _, err := client.CreateAppSession(ctx, def, "{}", quorumSigs) +\tif err != nil { log.Fatal(err) } +\tlog.Printf("Session created: %s", sessionID) + +\tinitVersion, err := strconv.ParseUint(versionStr, 10, 64) +\tif err != nil { log.Fatal(err) } + +\tdepositUpdate := app.AppStateUpdateV1{ +\t\tAppSessionID: sessionID, +\t\tIntent: app.AppStateUpdateIntentDeposit, +\t\tVersion: initVersion + 1, +\t\tAllocations: []app.AppAllocationV1{ +\t\t\t{Participant: myAddr, Asset: "usdc", Amount: decimal.NewFromInt(12)}, +\t\t\t{Participant: peer, Asset: "usdc", Amount: decimal.NewFromInt(8)}, +\t\t}, +\t\tSessionData: "{}", +\t} +\toperateSigs := []string{"0xMySig...", "0xPeerSig..."} +\tif _, err := client.SubmitAppSessionDeposit(ctx, depositUpdate, operateSigs, "usdc", decimal.NewFromInt(20)); err != nil { log.Fatal(err) } +\tlog.Println("Session funded with 20 USDC") + +\tupdate := app.AppStateUpdateV1{ +\t\tAppSessionID: sessionID, +\t\tIntent: app.AppStateUpdateIntentOperate, +\t\tVersion: initVersion + 2, +\t\tAllocations: depositUpdate.Allocations, +\t\tSessionData: "{\"round\":1,\"winner\":\"me\"}", +\t} +\tif err := client.SubmitAppState(ctx, update, operateSigs); err != nil { log.Fatal(err) } +\tlog.Println("State updated") + +\tupdate.Intent = app.AppStateUpdateIntentClose +\tupdate.Version = initVersion + 3 +\tcloseSigs := []string{"0xMyCloseSig...", "0xPeerCloseSig..."} +\tif err := client.SubmitAppState(ctx, update, closeSigs); err != nil { log.Fatal(err) } +\tlog.Println("Session closed") +} +`; + +const GO_SCAFFOLD_AI_AGENT = `package main + +import ( +\t"context" +\t"log" +\t"os" +\t"os/signal" +\t"syscall" +\t"time" + +\t"github.com/layer-3/nitrolite/pkg/sign" +\tsdk "github.com/layer-3/nitrolite/sdk/go" +\t"github.com/shopspring/decimal" +) + +func main() { +\tstateSigner, err := sign.NewEthereumMsgSigner(os.Getenv("AGENT_PRIVATE_KEY")) +\tif err != nil { log.Fatal(err) } +\ttxSigner, err := sign.NewEthereumRawSigner(os.Getenv("AGENT_PRIVATE_KEY")) +\tif err != nil { log.Fatal(err) } + +\tclient, err := sdk.NewClient(os.Getenv("CLEARNODE_URL"), stateSigner, txSigner, +\t\tsdk.WithBlockchainRPC(11155111, os.Getenv("RPC_URL")), +\t) +\tif err != nil { log.Fatal(err) } + +\tsigCh := make(chan os.Signal, 1) +\tsignal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) +\tgo func() { +\t\t<-sigCh +\t\tlog.Println("Shutting down agent...") +\t\tclient.Close() +\t}() + +\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) +\tdefer cancel() + +\t_, err = client.Deposit(ctx, 11155111, "usdc", decimal.NewFromInt(50)) +\tif err != nil { log.Fatal(err) } +\ttxHash, err := client.Checkpoint(ctx, "usdc") +\tif err != nil { log.Fatal(err) } +\tlog.Printf("Agent funded with 50 USDC, tx: %s", txHash) + +\trecipients := []string{"0x1111...", "0x2222..."} +\tfor _, r := range recipients { +\t\t_, err := client.Transfer(ctx, r, "usdc", decimal.NewFromFloat(0.10)) +\t\tif err != nil { +\t\t\tlog.Printf("Payment to %s failed: %v", r, err) +\t\t\tcontinue +\t\t} +\t\tlog.Printf("Paid 0.10 USDC to %s", r) +\t} + +\tlog.Println("Agent payments complete") +\t<-client.WaitCh() +} +`; + +// --------------------------------------------------------------------------- +// Initialize +// --------------------------------------------------------------------------- + +loadClientMethods(); +loadTypes(); +loadCompatExports(); +loadProtocolDocs(); +loadTerminology(); +loadV1API(); +loadGoTypes(); +loadGoSdkMethods(); + +// --------------------------------------------------------------------------- +// MCP Server +// --------------------------------------------------------------------------- + +const server = new McpServer({ + name: 'nitrolite-sdk', + version: '0.1.0', +}); + +// ========================== RESOURCES ====================================== + +server.resource('api-methods', 'nitrolite://api/methods', async () => { + const grouped: Record = {}; + for (const m of methods) { + (grouped[m.category] ??= []).push(m); + } + let text = '# Nitrolite SDK — Client Methods\n\n'; + for (const [cat, ms] of Object.entries(grouped).sort()) { + text += `## ${cat}\n\n`; + for (const m of ms) { + text += `### \`${m.signature}\`\n${m.description}\n\n`; + } + } + return { contents: [{ uri: 'nitrolite://api/methods', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('api-types', 'nitrolite://api/types', async () => { + const interfaces = types.filter(t => t.kind === 'interface'); + const aliases = types.filter(t => t.kind === 'type'); + let text = '# Nitrolite SDK — Types & Interfaces\n\n'; + text += `## Interfaces (${interfaces.length})\n\n`; + for (const t of interfaces) { + text += `### \`${t.name}\` (${t.source})\n\`\`\`typescript\n${t.fields}\n\`\`\`\n\n`; + } + text += `## Type Aliases (${aliases.length})\n\n`; + for (const t of aliases) { + text += `- \`${t.name}\` = \`${t.fields}\` (${t.source})\n`; + } + return { contents: [{ uri: 'nitrolite://api/types', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('api-enums', 'nitrolite://api/enums', async () => { + const enums = types.filter(t => t.kind === 'enum'); + let text = '# Nitrolite SDK — Enums\n\n'; + for (const e of enums) { + text += `## \`${e.name}\` (${e.source})\n\`\`\`typescript\n${e.fields}\n\`\`\`\n\n`; + } + return { contents: [{ uri: 'nitrolite://api/enums', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('examples-channels', 'nitrolite://examples/channels', async () => { + const text = `# Nitrolite SDK — Channel Examples + +## Creating a Channel & Depositing + +\`\`\`typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +const { stateSigner, txSigner } = createSigners('0xYourPrivateKey...'); +const client = await Client.create( + 'wss://clearnode.example.com/ws', + stateSigner, + txSigner, + withBlockchainRPC(80002n, 'https://rpc.amoy.polygon.technology'), +); + +// Deposit creates channel if needed +const state = await client.deposit(80002n, 'usdc', new Decimal(10)); +const txHash = await client.checkpoint('usdc'); +console.log('Deposit on-chain tx:', txHash); +\`\`\` + +## Querying Channels + +\`\`\`typescript +const userAddress = client.getUserAddress(); +const { channels } = await client.getChannels(userAddress); +for (const ch of channels) { + console.log(ch.channelId, ch.status); +} +\`\`\` + +## Closing a Channel + +\`\`\`typescript +// closeHomeChannel prepares the finalize state — checkpoint submits it on-chain +const finalState = await client.closeHomeChannel('usdc'); +const closeTx = await client.checkpoint('usdc'); +console.log('Channel closed, tx:', closeTx); +\`\`\` +`; + return { contents: [{ uri: 'nitrolite://examples/channels', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('examples-transfers', 'nitrolite://examples/transfers', async () => { + const text = `# Nitrolite SDK — Transfer Examples + +## Simple Transfer + +\`\`\`typescript +import Decimal from 'decimal.js'; + +const state = await client.transfer('0xRecipient...', 'usdc', new Decimal('5.0')); +console.log('Transfer tx ID:', state.transition.txId); +\`\`\` + +## Using the Compat Layer + +\`\`\`typescript +import { NitroliteClient } from '@yellow-org/sdk-compat'; + +const client = await NitroliteClient.create({ + wsURL: 'wss://clearnode.example.com/ws', + walletClient, + chainId: 11155111, + blockchainRPCs: { 11155111: 'https://rpc.sepolia.org' }, +}); + +await client.transfer('0xRecipient...', [{ asset: 'usdc', amount: '5.0' }]); +\`\`\` +`; + return { contents: [{ uri: 'nitrolite://examples/transfers', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('examples-app-sessions', 'nitrolite://examples/app-sessions', async () => { + const text = `# Nitrolite SDK — App Session Examples + +## Creating an App Session + +\`\`\`typescript +import { app } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +// Assumes client and CHAIN_ID are already defined. +await client.deposit(CHAIN_ID, 'usdc', new Decimal(20)); +await client.checkpoint('usdc'); + +// 1. Define the app session +const definition: app.AppDefinitionV1 = { + applicationId: 'my-game-app', + participants: [ + { walletAddress: '0xAlice...', signatureWeight: 50 }, + { walletAddress: '0xBob...', signatureWeight: 50 }, + ], + quorum: 100, // Both must agree + nonce: BigInt(Date.now()), +}; + +// 2. Collect quorum signatures from participants (off-band) +const quorumSigs: string[] = ['0xAliceSig...', '0xBobSig...']; + +// 3. Create the session +const result = await client.createAppSession(definition, '{}', quorumSigs); +console.log('Created session:', result.appSessionId); + +// 4. Fund the session before submitting non-zero allocations +const depositUpdate: app.AppStateUpdateV1 = { + appSessionId: result.appSessionId, + intent: app.AppStateUpdateIntent.Deposit, + version: 2n, + allocations: [ + { participant: '0xAlice...', asset: 'usdc', amount: new Decimal(15) }, + { participant: '0xBob...', asset: 'usdc', amount: new Decimal(5) }, + ], + sessionData: '{}', +}; +await client.submitAppSessionDeposit(depositUpdate, ['0xAliceSig...', '0xBobSig...'], 'usdc', new Decimal(20)); +\`\`\` + +## Submitting App State + +\`\`\`typescript +import Decimal from 'decimal.js'; + +const appUpdate: app.AppStateUpdateV1 = { + appSessionId: result.appSessionId, + intent: app.AppStateUpdateIntent.Operate, + version: 3n, + allocations: [ + { participant: '0xAlice...', asset: 'usdc', amount: new Decimal(15) }, + { participant: '0xBob...', asset: 'usdc', amount: new Decimal(5) }, + ], + sessionData: '{"move": "e4"}', +}; +await client.submitAppState(appUpdate, ['0xAliceSig...', '0xBobSig...']); +\`\`\` + +> **Note:** There is no \`closeAppSession()\` on the SDK Client. To close a session, +> submit a state update with \`intent: app.AppStateUpdateIntent.Close\`. +`; + return { contents: [{ uri: 'nitrolite://examples/app-sessions', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('examples-auth', 'nitrolite://examples/auth', async () => { + const text = `# Nitrolite SDK — Authentication Examples + +## Compat Layer Auth Flow (Legacy v0.5.3 Pattern) + +\`\`\`typescript +import { + createAuthRequestMessage, + createAuthVerifyMessage, + createEIP712AuthMessageSigner, + parseAnyRPCResponse, + type AuthRequestParams, +} from '@yellow-org/sdk-compat'; + +// 1. Create auth request +const authParams: AuthRequestParams = { + address: account.address, + session_key: '0x0000000000000000000000000000000000000000', + application: 'My App', + expires_at: BigInt(Math.floor(Date.now() / 1000) + 3600), + scope: 'app.create', + allowances: [{ asset: 'usdc', amount: '100.0' }], +}; +const authMsg = await createAuthRequestMessage(authParams); +ws.send(authMsg); + +// 2. Parse challenge +const challengeRaw = await waitForResponse(); +const challenge = parseAnyRPCResponse(challengeRaw); + +// 3. Create EIP-712 signer and verify +const signer = createEIP712AuthMessageSigner( + walletClient, + { scope: authParams.scope, session_key: authParams.session_key as \\\`0x\${string}\\\`, expires_at: authParams.expires_at, allowances: authParams.allowances }, + { name: 'Yellow Network' }, +); +const verifyMsg = await createAuthVerifyMessage(signer, challenge); +ws.send(verifyMsg); + +// 4. Parse verification result +const verifyRaw = await waitForResponse(); +const result = parseAnyRPCResponse(verifyRaw); +console.log('Authenticated:', result); +\`\`\` +`; + return { contents: [{ uri: 'nitrolite://examples/auth', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('migration-overview', 'nitrolite://migration/overview', async () => { + const content = readFile(resolve(COMPAT_ROOT, 'docs/migration-overview.md')); + const text = content || '# Migration Overview\n\nNo migration docs found. Check sdk/ts-compat/docs/.'; + return { contents: [{ uri: 'nitrolite://migration/overview', text, mimeType: 'text/markdown' }] }; +}); + +// ========================== PROTOCOL RESOURCES ============================== + +server.resource('protocol-overview', 'nitrolite://protocol/overview', async () => { + const text = protocolDocs['overview'] || '# Protocol Overview\n\nProtocol docs not found.'; + return { contents: [{ uri: 'nitrolite://protocol/overview', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('protocol-terminology', 'nitrolite://protocol/terminology', async () => { + const text = protocolDocs['terminology'] || '# Terminology\n\nTerminology docs not found.'; + return { contents: [{ uri: 'nitrolite://protocol/terminology', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('protocol-wire-format', 'nitrolite://protocol/wire-format', async () => { + const text = protocolDocs['interactions'] || '# Wire Format\n\nInteraction docs not found.'; + return { contents: [{ uri: 'nitrolite://protocol/wire-format', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('protocol-rpc-methods', 'nitrolite://protocol/rpc-methods', async () => { + let text = '# V1 RPC Methods\n\nAll v1 RPC methods defined in `docs/api.yaml`. Methods use grouped naming: `{group}.v1.{method}`.\n\n'; + + // Group methods by their API group + const grouped: Record = {}; + for (const doc of rpcMethodDocs.values()) { + (grouped[doc.group] ??= []).push(doc); + } + for (const [group, docs] of Object.entries(grouped)) { + text += `## ${group}\n\n`; + text += '| Method | Description | Request Fields | Response Fields |\n|---|---|---|---|\n'; + for (const doc of docs) { + text += `| \`${doc.method}\` | ${doc.description} | ${doc.requestFields} | ${doc.responseFields} |\n`; + } + text += '\n'; + } + + text += '## Message Format\n\n'; + text += 'All messages use compact ordered arrays:\n\n'; + text += '**Request:** `{ "req": [REQUEST_ID, METHOD, PARAMS, TIMESTAMP], "sig": ["SIGNATURE"] }`\n\n'; + text += '**Response:** `{ "res": [REQUEST_ID, METHOD, DATA, TIMESTAMP], "sig": ["SIGNATURE"] }`\n\n'; + text += '**With App Session:** Add `"sid": "APP_SESSION_ID"` to route messages to app session participants.\n'; + return { contents: [{ uri: 'nitrolite://protocol/rpc-methods', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('protocol-cryptography', 'nitrolite://protocol/cryptography', async () => { + const text = protocolDocs['cryptography'] || '# Cryptography\n\nCryptography docs not found.'; + return { contents: [{ uri: 'nitrolite://protocol/cryptography', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('protocol-channel-lifecycle', 'nitrolite://protocol/channel-lifecycle', async () => { + const text = protocolDocs['channel-protocol'] || '# Channel Protocol\n\nChannel protocol docs not found.'; + return { contents: [{ uri: 'nitrolite://protocol/channel-lifecycle', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('protocol-state-model', 'nitrolite://protocol/state-model', async () => { + const text = protocolDocs['state-model'] || '# State Model\n\nState model docs not found.'; + return { contents: [{ uri: 'nitrolite://protocol/state-model', text, mimeType: 'text/markdown' }] }; +}); + +// ========================== SECURITY RESOURCES ============================== + +server.resource('security-overview', 'nitrolite://security/overview', async () => { + const text = protocolDocs['security-and-limitations'] || '# Security\n\nSecurity docs not found.'; + return { contents: [{ uri: 'nitrolite://security/overview', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('security-app-session-patterns', 'nitrolite://security/app-session-patterns', async () => { + const text = `# App Session Security Patterns + +Best practices for building secure, decentralization-ready app sessions on Nitrolite. + +## Quorum Design + +App sessions use a weight-based quorum system for governance: + +\`\`\`typescript +interface AppDefinitionV1 { + applicationId: string; + participants: AppParticipantV1[]; // each has walletAddress + signatureWeight + quorum: number; // minimum total weight to authorize actions (uint8) + nonce: bigint; +} +\`\`\` + +### Recommended Patterns + +**Equal 2-of-2 (peer-to-peer):** Both participants must agree. +\`\`\`json +{ "participants": [{ "signatureWeight": 50 }, { "signatureWeight": 50 }], "quorum": 100 } +\`\`\` + +**3-of-3 (multi-party unanimous):** All three must agree. +\`\`\`json +{ "participants": [{ "signatureWeight": 34 }, { "signatureWeight": 33 }, { "signatureWeight": 33 }], "quorum": 100 } +\`\`\` + +**2-of-3 with arbitrator:** Any two can authorize (third party can break ties). +\`\`\`json +{ "participants": [{ "signatureWeight": 50 }, { "signatureWeight": 50 }, { "signatureWeight": 50 }], "quorum": 100 } +\`\`\` + +**Weighted (operator-controlled):** One party has majority weight. +\`\`\`json +{ "participants": [{ "signatureWeight": 70 }, { "signatureWeight": 30 }], "quorum": 70 } +\`\`\` + +## Challenge Periods + +The challenge duration defines how long a dispute can be contested on-chain: +- **Short (1 hour):** For low-value, high-frequency operations +- **Medium (24 hours):** Recommended default for most applications +- **Long (7 days):** For high-value operations requiring more security + +## State Invariants + +Developers MUST ensure these invariants hold in every state update: +1. **Fund conservation:** Total allocations across participants MUST equal the committed amount +2. **Version ordering:** Each state version MUST be exactly previous + 1 +3. **Signature requirements:** Updates require signatures meeting the quorum threshold +4. **Non-negative allocations:** No participant's allocation can go below zero + +## Decentralization-Ready Patterns + +Even if not fully decentralized today, build app sessions so they would work in a decentralized environment: + +1. **Never trust a single party** — Use quorum >= total weight of any single participant +2. **Use challenge periods** — They exist to protect against malicious state submissions +3. **Keep state deterministic** — All participants should be able to independently verify state transitions +4. **Support unilateral enforcement** — Any participant should be able to enforce the latest agreed state on-chain +5. **Separate signing from logic** — Use session keys with spending caps rather than raw wallet keys + +## On-Chain Enforcement + +If off-chain cooperation fails, any participant can: +1. Submit the latest mutually signed state to the blockchain +2. The blockchain validates signatures, version ordering, and ledger invariants +3. After the challenge period, the state becomes final +4. Assets are distributed according to the final allocations +`; + return { contents: [{ uri: 'nitrolite://security/app-session-patterns', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('security-state-invariants', 'nitrolite://security/state-invariants', async () => { + const text = `# State Invariants + +Critical invariants that MUST hold across all state transitions. Violating these will cause on-chain enforcement to fail. + +## Ledger Invariant (Fund Conservation) + +\`\`\` +UserAllocation + NodeAllocation == UserNetFlow + NodeNetFlow +\`\`\` + +This ensures no assets can be created or destroyed through state transitions. The total distributable balance always equals the total cumulative flows. + +## Allocation Non-Negativity + +All allocation values (UserAllocation, NodeAllocation) MUST be non-negative. Net flow values MAY be negative (outbound transfers exceeding inbound). + +## Version Ordering + +- **Off-chain:** Each new version MUST equal previous version + 1 +- **On-chain enforcement:** Submitted version MUST be strictly greater than the current on-chain version + +## Signature Requirements + +- **Mutually signed states** (both user + node signatures) are the only states enforceable on-chain +- **Node-issued pending states** (node signature only) are NOT enforceable — they become enforceable after user acknowledgement +- Signature validation modes MUST be among the channel's approved validators + +## Channel Binding + +The channel identifier in every state MUST match the channel definition. This is verified both off-chain and on-chain. + +## Locked Funds + +Unless the channel is being closed: +\`\`\` +UserAllocation + NodeAllocation == LockedFunds +\`\`\` + +Locked funds MUST never be negative. The node MUST have sufficient vault funds when required to lock additional assets. + +## Empty Non-Home Ledger + +For non-cross-chain operations, the non-home ledger MUST be fully zeroed (all fields set to 0/zero-address). +`; + return { contents: [{ uri: 'nitrolite://security/state-invariants', text, mimeType: 'text/markdown' }] }; +}); + +// ========================== USE CASES RESOURCES ============================= + +server.resource('use-cases', 'nitrolite://use-cases', async () => { + const text = `# Nitrolite Use Cases + +What you can build with the Nitrolite SDK and state channels. + +## Peer-to-Peer Payments +Instant, gas-free token transfers between users. Open a channel, transfer any amount instantly, settle on-chain only when needed. +**SDK methods:** \`client.deposit()\`, \`client.transfer()\`, \`client.closeHomeChannel()\` + +## Gaming (Real-Time Wagering) +Turn-based or real-time games where players wager tokens. App sessions track game state; winners receive payouts automatically. +**SDK methods:** \`client.createAppSession()\`, \`client.submitAppSessionDeposit()\`, \`client.submitAppState()\` (close via \`submitAppState\` with Close intent) +**Example:** Yetris — a Tetris-style game with token wagering built on app sessions. + +## Multi-Party Checkout / Escrow +Multiple parties contribute to a shared pool (e.g., group payment, crowdfunding). Funds release when quorum conditions are met. +**SDK methods:** \`client.createAppSession()\`, \`client.submitAppSessionDeposit()\`, custom quorum weights, close via \`client.submitAppState()\` with Close intent +**Example:** Cosign Demo — a multi-party co-signing checkout flow. + +## AI Agent Payments +Autonomous AI agents making and receiving payments via state channels. Agents manage their own wallets, open channels, and transact programmatically. +**SDK methods:** \`Client.create()\`, \`client.deposit()\`, \`client.transfer()\` +**See also:** \`nitrolite://use-cases/ai-agents\` + +## DeFi Escrow & Atomic Swaps +Trustless exchange of assets between parties using escrow transitions. Cross-chain support via the unified asset model. +**SDK methods:** Escrow transitions via \`client.submitAppSessionDeposit()\` + +## Streaming Payments +Continuous micro-transfers over time (e.g., pay-per-second for compute, bandwidth, or content). State channels make sub-cent payments feasible. +**SDK methods:** \`client.transfer()\` in a loop with small amounts + +## Cross-Chain Operations +Move assets between blockchains through the escrow mechanism. Deposit on chain A, use on chain B, withdraw on chain C. +**SDK methods:** Cross-chain escrow transitions, \`client.deposit()\` on any supported chain +`; + return { contents: [{ uri: 'nitrolite://use-cases', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('use-cases-ai-agents', 'nitrolite://use-cases/ai-agents', async () => { + const text = `# AI Agent Use Cases + +How to use Nitrolite for AI agent payments and agent-to-agent interactions. + +## Why State Channels for AI Agents? + +AI agents need to make frequent, small payments — often thousands per session. On-chain transactions are too slow and expensive. State channels provide: +- **Instant finality** — no waiting for block confirmations +- **Near-zero cost** — gas only on channel open/close, not per-transfer +- **Programmable** — agents manage channels autonomously via the SDK + +## Agent Wallet Setup + +\`\`\`typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; + +// Agent has its own private key +const { stateSigner, txSigner } = createSigners(AGENT_PRIVATE_KEY); + +const client = await Client.create( + 'wss://clearnode.example.com/ws', + stateSigner, + txSigner, + withBlockchainRPC(chainId, RPC_URL), +); +\`\`\` + +## Agent-to-Agent Payments + +Two AI agents can transact directly through state channels: +1. Both agents open channels with the same clearnode +2. Agent A calls \`client.transfer(agentB_address, 'usdc', new Decimal('0.01'))\` +3. Agent B receives the transfer instantly +4. No on-chain transactions needed + +## Session Key Delegation + +For security, agents can use delegated session keys with spending caps: +- The agent's main wallet authorizes a session key during authentication +- The session key has a maximum spending allowance (e.g., 100 USDC) +- Once the cap is reached, the session key is revoked +- The main wallet funds are never at risk beyond the allowance + +## Autonomous Escrow + +AI agents can participate in app sessions for complex multi-step workflows: +1. Agent creates an app session with another agent or user +2. Both commit funds to the session +3. The application logic determines final allocations +4. The session closes and funds are distributed + +## Integration with Agent Frameworks + +The SDK works with any agent framework (LangChain, AutoGPT, CrewAI, etc.): +- Wrap SDK methods as agent tools +- Let the agent decide when to make payments +- Use session keys for safe autonomous operation + +## yao.com Proxy Pattern + +For agents that need a unified interface, yao.com provides a proxy layer: +- Agents connect to yao.com instead of directly to a clearnode +- yao.com handles channel management and routing +- Agents focus on their application logic +`; + return { contents: [{ uri: 'nitrolite://use-cases/ai-agents', text, mimeType: 'text/markdown' }] }; +}); + +// ========================== FULL EXAMPLE RESOURCES =========================== + +server.resource('examples-full-transfer', 'nitrolite://examples/full-transfer-script', async () => { + const text = `# Complete Transfer Script + +A fully working TypeScript script that connects to a clearnode, opens a channel, deposits funds, transfers tokens, and closes the channel. + +\`\`\`typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +// --- Configuration --- +const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\${string}\`; +const CLEARNODE_URL = process.env.CLEARNODE_URL || 'wss://clearnode.example.com/ws'; +const RPC_URL = process.env.RPC_URL || 'https://rpc.sepolia.org'; +const CHAIN_ID = 80002n; // Polygon Amoy +const RECIPIENT = process.env.RECIPIENT as \`0x\${string}\`; + +async function main() { + // 1. Create signers from private key + const { stateSigner, txSigner } = createSigners(PRIVATE_KEY); + + // 2. Create SDK client — connects WebSocket + authenticates + const client = await Client.create( + CLEARNODE_URL, + stateSigner, + txSigner, + withBlockchainRPC(CHAIN_ID, RPC_URL), + ); + console.log('Connected and authenticated'); + + // 3. Approve token spending (one-time per token, or when increasing allowance) + await client.approveToken(CHAIN_ID, 'usdc', new Decimal(1000)); + console.log('Token approved'); + + // 4. Deposit — creates channel if needed, then checkpoint on-chain + const depositState = await client.deposit(CHAIN_ID, 'usdc', new Decimal(10)); + const depositTx = await client.checkpoint('usdc'); + console.log('Deposited 10 USDC, tx:', depositTx); + + // 5. Transfer to recipient + const transferState = await client.transfer(RECIPIENT, 'usdc', new Decimal(5)); + console.log('Transferred 5 USDC, state version:', transferState.version); + + // 6. Check balances + const userAddress = client.getUserAddress(); + const balances = await client.getBalances(userAddress); + console.log('Current balances:', balances); + + // 7. Close channel — two steps: prepare finalize state, then checkpoint on-chain + const finalState = await client.closeHomeChannel('usdc'); + const closeTx = await client.checkpoint('usdc'); + console.log('Channel closed, tx:', closeTx); +} + +main().catch(console.error); +\`\`\` + +## Environment Variables + +- \`PRIVATE_KEY\` — Your wallet private key (hex with 0x prefix) +- \`CLEARNODE_URL\` — WebSocket URL of the clearnode +- \`RPC_URL\` — Ethereum RPC endpoint for the target chain +- \`RECIPIENT\` — Address to transfer tokens to + +## Dependencies + +\`\`\`json +{ + "@yellow-org/sdk": "^1.2.0", + "decimal.js": "^10.4.0", + "viem": "^2.46.0" +} +\`\`\` +`; + return { contents: [{ uri: 'nitrolite://examples/full-transfer-script', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('examples-full-app-session', 'nitrolite://examples/full-app-session-script', async () => { + const text = `# Complete App Session Script + +A fully working TypeScript script that creates a multi-party app session, submits state updates, and closes with final allocations. + +\`\`\`typescript +import { Client, createSigners, withBlockchainRPC, app } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +// --- Configuration --- +const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\${string}\`; +const CLEARNODE_URL = process.env.CLEARNODE_URL || 'wss://clearnode.example.com/ws'; +const RPC_URL = process.env.RPC_URL || 'https://rpc.sepolia.org'; +const CHAIN_ID = 80002n; +const PEER_ADDRESS = process.env.PEER_ADDRESS as \`0x\${string}\`; + +async function main() { + const { stateSigner, txSigner } = createSigners(PRIVATE_KEY); + const myAddress = stateSigner.address; + + const client = await Client.create( + CLEARNODE_URL, + stateSigner, + txSigner, + withBlockchainRPC(CHAIN_ID, RPC_URL), + ); + console.log('Connected'); + + // Ensure funds are available in the home channel before app-session funding + await client.deposit(CHAIN_ID, 'usdc', new Decimal(20)); + await client.checkpoint('usdc'); + + // 1. Define app session + const definition: app.AppDefinitionV1 = { + applicationId: 'my-game-app', + participants: [ + { walletAddress: myAddress, signatureWeight: 50 }, + { walletAddress: PEER_ADDRESS, signatureWeight: 50 }, + ], + quorum: 100, // Both must agree + nonce: BigInt(Date.now()), + }; + + // 2. Collect quorum signatures from participants (off-band signing) + const quorumSigs: string[] = ['0xMySignature...', '0xPeerSignature...']; + + // 3. Create app session + const session = await client.createAppSession(definition, '{}', quorumSigs); + console.log('App session created:', session.appSessionId); + + // 4. Fund the app session before submitting non-zero allocations + const depositUpdate: app.AppStateUpdateV1 = { + appSessionId: session.appSessionId, + intent: app.AppStateUpdateIntent.Deposit, + version: 2n, + allocations: [ + { participant: myAddress, asset: 'usdc', amount: new Decimal(15) }, + { participant: PEER_ADDRESS, asset: 'usdc', amount: new Decimal(5) }, + ], + sessionData: '{}', + }; + await client.submitAppSessionDeposit(depositUpdate, ['0xMySig...', '0xPeerSig...'], 'usdc', new Decimal(20)); + console.log('Session funded with 20 USDC'); + + // 5. Submit state update — e.g., after a game round + const appUpdate: app.AppStateUpdateV1 = { + appSessionId: session.appSessionId, + intent: app.AppStateUpdateIntent.Operate, + version: 3n, + allocations: depositUpdate.allocations, + sessionData: '{"round": 1, "winner": "me"}', + }; + await client.submitAppState(appUpdate, ['0xMySig...', '0xPeerSig...']); + console.log('State updated: I won 5 USDC'); + + // 6. Close app session — submit final state with Close intent + const closeUpdate: app.AppStateUpdateV1 = { + ...appUpdate, + intent: app.AppStateUpdateIntent.Close, + version: 4n, + }; + await client.submitAppState(closeUpdate, ['0xMyCloseSig...', '0xPeerCloseSig...']); + console.log('Session closed, funds returned to channels'); +} + +main().catch(console.error); +\`\`\` + +## Key Concepts + +- **Quorum:** Set to 100 with equal weights (50/50) — both parties must sign every state update +- **Allocations:** Must always sum to the total committed amount (fund conservation invariant) +- **Intent:** Use \`Operate\` for normal updates, \`Close\` for final settlement (there is no separate \`closeAppSession()\` method) +- **Session data:** Optional string field for app-specific metadata (game state, etc.) +- **Quorum sigs:** Participants sign the app state off-band; signatures are collected and submitted together + +## Dependencies + +\`\`\`json +{ + "@yellow-org/sdk": "^1.2.0", + "viem": "^2.46.0", + "decimal.js": "^10.6.0" +} +\`\`\` +`; + return { contents: [{ uri: 'nitrolite://examples/full-app-session-script', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('protocol-enforcement', 'nitrolite://protocol/enforcement', async () => { + const text = protocolDocs['enforcement'] || '# Enforcement\n\nEnforcement docs not found.'; + return { contents: [{ uri: 'nitrolite://protocol/enforcement', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('protocol-cross-chain', 'nitrolite://protocol/cross-chain', async () => { + const text = protocolDocs['cross-chain-and-assets'] || '# Cross-Chain & Assets\n\nCross-chain docs not found.'; + return { contents: [{ uri: 'nitrolite://protocol/cross-chain', text, mimeType: 'text/markdown' }] }; +}); + +server.resource('protocol-interactions', 'nitrolite://protocol/interactions', async () => { + const text = protocolDocs['interactions'] || '# Interactions\n\nInteractions docs not found.'; + return { contents: [{ uri: 'nitrolite://protocol/interactions', text, mimeType: 'text/markdown' }] }; +}); + +// ========================== GO SDK RESOURCES ================================ + +server.resource('go-api-methods', 'nitrolite://go-api/methods', async () => ({ + contents: [{ uri: 'nitrolite://go-api/methods', text: buildGoApiMethodsContent(), mimeType: 'text/markdown' }], +})); + +server.resource('go-api-types', 'nitrolite://go-api/types', async () => ({ + contents: [{ uri: 'nitrolite://go-api/types', text: buildGoTypesContent(), mimeType: 'text/markdown' }], +})); + +server.resource('go-examples-full-transfer', 'nitrolite://go-examples/full-transfer-script', async () => ({ + contents: [{ uri: 'nitrolite://go-examples/full-transfer-script', text: GO_TRANSFER_EXAMPLE, mimeType: 'text/markdown' }], +})); + +server.resource('go-examples-full-app-session', 'nitrolite://go-examples/full-app-session-script', async () => ({ + contents: [{ uri: 'nitrolite://go-examples/full-app-session-script', text: GO_APP_SESSION_EXAMPLE, mimeType: 'text/markdown' }], +})); + +server.resource('protocol-auth-flow', 'nitrolite://protocol/auth-flow', async () => ({ + contents: [{ uri: 'nitrolite://protocol/auth-flow', text: AUTH_FLOW_CONTENT, mimeType: 'text/markdown' }], +})); + + +// ========================== TOOLS ========================================== + +server.tool( + 'lookup_method', + 'Look up a specific SDK Client method by name — returns signature, params, return type, usage context', + { + name: z.string().describe('Method name (e.g. "transfer", "deposit", "getChannels", "Transfer", "Deposit")'), + language: z.enum(['typescript', 'go', 'both']).optional().default('typescript').describe('SDK language to search: "typescript" (default), "go", or "both"'), + }, + async ({ name, language }) => { + const query = name.toLowerCase(); + const parts: string[] = []; + + if (language === 'typescript' || language === 'both') { + const tsMatches = methods.filter(m => m.name.toLowerCase().includes(query)); + if (tsMatches.length > 0) { + const header = language === 'both' ? '## TypeScript SDK\n\n' : ''; + parts.push(header + tsMatches.map(m => + `### ${m.name}\n**Signature:** \`${m.signature}\`\n**Category:** ${m.category}\n**Description:** ${m.description}` + ).join('\n\n---\n\n')); + } + } + + if (language === 'go' || language === 'both') { + const goMatches = goMethods.filter(m => m.name.toLowerCase().includes(query)); + if (goMatches.length > 0) { + const header = language === 'both' ? '## Go SDK\n\n' : ''; + parts.push(header + goMatches.map(m => + `### ${m.name}\n**Signature:**\n\`\`\`go\n${m.signature}\n\`\`\`\n**Category:** ${m.category}\n**Description:** ${m.comment}` + ).join('\n\n---\n\n')); + } + } + + if (parts.length === 0) { + return { content: [{ type: 'text' as const, text: `No method matching "${name}" found. Available categories: ${[...new Set(methods.map(m => m.category))].join(', ')}` }] }; + } + return { content: [{ type: 'text' as const, text: parts.join('\n\n') }] }; + }, +); + +server.tool( + 'lookup_type', + 'Look up a type, interface, or enum by name — returns fields and source location', + { + name: z.string().describe('Type name (e.g. "Channel", "State", "RPCMethod", "AppSessionV1", "ChannelStatus")'), + language: z.enum(['typescript', 'go', 'both']).optional().default('typescript').describe('SDK language to search: "typescript" (default), "go", or "both"'), + }, + async ({ name, language }) => { + const query = name.toLowerCase(); + const parts: string[] = []; + + if (language === 'typescript' || language === 'both') { + const tsMatches = types.filter(t => t.name.toLowerCase().includes(query)); + if (tsMatches.length > 0) { + const header = language === 'both' ? '## TypeScript SDK\n\n' : ''; + parts.push(header + tsMatches.map(t => + `### ${t.name} (${t.kind})\n**Source:** ${t.source}\n\`\`\`typescript\n${t.fields}\n\`\`\`` + ).join('\n\n---\n\n')); + } + } + + if (language === 'go' || language === 'both') { + const goMatches = goTypes.filter(t => t.name.toLowerCase().includes(query)); + if (goMatches.length > 0) { + const header = language === 'both' ? '## Go SDK\n\n' : ''; + parts.push(header + goMatches.map(t => { + if (t.kind === 'struct') { + return `### ${t.name} (struct)\n**Source:** ${t.source}\n\`\`\`go\ntype ${t.name} struct {\n${t.fields}\n}\n\`\`\``; + } else if (t.kind === 'enum') { + return `### ${t.name} (enum)\n**Source:** ${t.source}\n**Values:**\n${t.fields.split('\n').map(v => `- \`${v}\``).join('\n')}`; + } + return `### ${t.name} (${t.kind})\n**Source:** ${t.source}\n\`\`\`go\ntype ${t.name} ${t.fields}\n\`\`\``; + }).join('\n\n---\n\n')); + } + } + + if (parts.length === 0) { + return { content: [{ type: 'text' as const, text: `No type matching "${name}" found. ${types.length} TS types and ${goTypes.length} Go types indexed.` }] }; + } + return { content: [{ type: 'text' as const, text: parts.join('\n\n') }] }; + }, +); + +server.tool( + 'search_api', + 'Fuzzy search across all SDK methods and types', + { + query: z.string().describe('Search query (e.g. "session key", "balance", "transfer", "AppSession")'), + language: z.enum(['typescript', 'go', 'both']).optional().default('typescript').describe('SDK language to search: "typescript" (default), "go", or "both"'), + }, + async ({ query, language }) => { + const q = query.toLowerCase(); + let text = `# Search results for "${query}"\n\n`; + let totalHits = 0; + + if (language === 'typescript' || language === 'both') { + const methodHits = methods.filter(m => + m.name.toLowerCase().includes(q) || m.description.toLowerCase().includes(q) || m.category.toLowerCase().includes(q) + ); + const typeHits = types.filter(t => + t.name.toLowerCase().includes(q) || t.fields.toLowerCase().includes(q) + ); + const prefix = language === 'both' ? 'TypeScript SDK ' : ''; + if (methodHits.length > 0) { + text += `## ${prefix}Methods (${methodHits.length} matches)\n`; + for (const m of methodHits.slice(0, 10)) text += `- \`${m.signature}\` — ${m.category}\n`; + text += '\n'; + totalHits += methodHits.length; + } + if (typeHits.length > 0) { + text += `## ${prefix}Types (${typeHits.length} matches)\n`; + for (const t of typeHits.slice(0, 10)) text += `- \`${t.name}\` (${t.kind}) — ${t.source}\n`; + text += '\n'; + totalHits += typeHits.length; + } + } + + if (language === 'go' || language === 'both') { + const goMethodHits = goMethods.filter(m => + m.name.toLowerCase().includes(q) || m.comment.toLowerCase().includes(q) || m.category.toLowerCase().includes(q) + ); + const goTypeHits = goTypes.filter(t => + t.name.toLowerCase().includes(q) || t.fields.toLowerCase().includes(q) + ); + const prefix = language === 'both' ? 'Go SDK ' : ''; + if (goMethodHits.length > 0) { + text += `## ${prefix}Methods (${goMethodHits.length} matches)\n`; + for (const m of goMethodHits.slice(0, 10)) text += `- \`${m.name}\` — ${m.category}\n`; + text += '\n'; + totalHits += goMethodHits.length; + } + if (goTypeHits.length > 0) { + text += `## ${prefix}Types (${goTypeHits.length} matches)\n`; + for (const t of goTypeHits.slice(0, 10)) text += `- \`${t.name}\` (${t.kind}) — ${t.source}\n`; + text += '\n'; + totalHits += goTypeHits.length; + } + } + + if (totalHits === 0) text += 'No matches found. Try a broader term.\n'; + return { content: [{ type: 'text' as const, text }] }; + }, +); + +server.tool( + 'get_rpc_method', + 'Get the RPC wire format for a 0.5.x compat-layer method and its v1 equivalent. For v1 method reference, see docs/api.yaml.', + { method: z.string().describe('0.5.x compat method name (e.g. "get_channels", "transfer", "create_app_session")') }, + async ({ method }) => { + // NOTE: These are 0.5.x compat-layer method names mapped to their v1 wire equivalents. + // The v1 API uses grouped methods (e.g. channels.v1.submit_state). The canonical v1 + // reference is docs/api.yaml. This tool exists for sdk-compat integration test authors. + const rpcMethods: Record = { + ping: { wireMethod: 'node.v1.ping', reqFormat: '{ req: [requestId, "ping", {}, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "ping", { pong: true }] }' }, + get_channels: { wireMethod: 'channels.v1.get_channels', reqFormat: '{ req: [requestId, "get_channels", { wallet?, status?, asset?, channel_type?, pagination? }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "get_channels", { channels: [...], metadata: {...} }] }' }, + get_ledger_balances: { wireMethod: 'user.v1.get_balances', reqFormat: '{ req: [requestId, "get_ledger_balances", { wallet }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "get_ledger_balances", { balances: RPCBalance[] }] }' }, + transfer: { wireMethod: 'channels.v1.submit_state', reqFormat: '{ req: [requestId, "transfer", { destination, allocations: [{ asset, amount }] }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "transfer", { state }] }' }, + create_channel: { wireMethod: 'channels.v1.request_creation', reqFormat: '{ req: [requestId, "create_channel", [{ chain_id, token }], timestamp], sig: [...] }', resFormat: '{ res: [requestId, "create_channel", [{ channel_id, channel, state, server_signature }], timestamp], sig: [...] }' }, + close_channel: { wireMethod: 'channels.v1.submit_state', reqFormat: '{ req: [requestId, "close_channel", { channel_id, funds_destination }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "close_channel", { channel_id, state, server_signature }] }' }, + create_app_session: { wireMethod: 'app_sessions.v1.create_app_session', reqFormat: '{ req: [requestId, "create_app_session", { definition, session_data, quorum_sigs, owner_sig? }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "create_app_session", { app_session_id, version, status }] }' }, + submit_app_state: { wireMethod: 'app_sessions.v1.submit_app_state', reqFormat: '{ req: [requestId, "submit_app_state", { app_state_update, quorum_sigs }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "submit_app_state", { accepted: boolean }] }' }, + get_app_sessions: { wireMethod: 'app_sessions.v1.get_app_sessions', reqFormat: '{ req: [requestId, "get_app_sessions", { filters? }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "get_app_sessions", { sessions: AppSession[] }] }' }, + get_app_definition: { wireMethod: 'app_sessions.v1.get_app_definition', reqFormat: '{ req: [requestId, "get_app_definition", { app_session_id }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "get_app_definition", { definition }] }' }, + get_ledger_transactions: { wireMethod: 'user.v1.get_transactions', reqFormat: '{ req: [requestId, "get_ledger_transactions", { wallet, filters? }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "get_ledger_transactions", { transactions: RPCTransaction[] }] }' }, + resize_channel: { wireMethod: 'channels.v1.submit_state', reqFormat: '{ req: [requestId, "resize_channel", { channel_id, resize_amount, allocate_amount, funds_destination }, timestamp], sig: [...] }', resFormat: '{ res: [requestId, "resize_channel", { channel_id, state, server_signature }] }' }, + }; + + const key = method.toLowerCase(); + const info = rpcMethods[key]; + if (!info) { + return { content: [{ type: 'text' as const, text: `Unknown RPC method "${method}". Available: ${Object.keys(rpcMethods).join(', ')}` }] }; + } + const text = `## RPC: ${method}\n\n**V1 Wire Method:** \`${info.wireMethod}\`\n\n**Request format (v0.5.3 compat):**\n\`\`\`\n${info.reqFormat}\n\`\`\`\n\n**Response format:**\n\`\`\`\n${info.resFormat}\n\`\`\``; + return { content: [{ type: 'text' as const, text }] }; + }, +); + +server.tool( + 'validate_import', + 'Check if a symbol is exported from sdk-compat barrel — returns yes/no + correct import path', + { symbol: z.string().describe('Symbol name (e.g. "NitroliteClient", "RPCMethod", "createTransferMessage")') }, + async ({ symbol }) => { + const compatMatch = findNamedExport(compatExports, symbol); + if (compatMatch.found) { + return { content: [{ type: 'text' as const, text: `**${symbol}** is exported from \`@yellow-org/sdk-compat\`.\n\n\`\`\`typescript\n${renderImportStatement('@yellow-org/sdk-compat', symbol, compatMatch.isTypeOnly)}\n\`\`\`` }] }; + } + + // Check if it's in the main SDK + const sdkBarrelContent = readFile(resolve(SDK_ROOT, 'src/index.ts')); + const sdkExports = extractExports(sdkBarrelContent); + const sdkMatch = findNamedExport(sdkExports, symbol); + if (sdkMatch.found) { + return { content: [{ type: 'text' as const, text: `**${symbol}** is NOT in \`@yellow-org/sdk-compat\` but IS in \`@yellow-org/sdk\`.\n\n\`\`\`typescript\n${renderImportStatement('@yellow-org/sdk', symbol, sdkMatch.isTypeOnly)}\n\`\`\`\n\n> Note: SDK classes should not be re-exported from compat (SSR risk). Import directly from \`@yellow-org/sdk\`.` }] }; + } + + return { content: [{ type: 'text' as const, text: `**${symbol}** was not found in either \`@yellow-org/sdk-compat\` or \`@yellow-org/sdk\` barrel exports. It may be a deep import or may not exist.` }] }; + }, +); + +server.tool( + 'explain_concept', + 'Plain-English explanation of a Nitrolite protocol concept (e.g. "state channel", "app session", "challenge period")', + { concept: z.string().describe('Concept name (e.g. "state channel", "app session", "challenge period", "clearnode", "vault")') }, + async ({ concept }) => { + const query = concept.toLowerCase().trim(); + + // Direct match + const direct = concepts.get(query); + if (direct) { + return { content: [{ type: 'text' as const, text: direct }] }; + } + + // Fuzzy match — find concepts that contain the query or vice versa + const matches: string[] = []; + for (const [key, value] of concepts) { + if (key.includes(query) || query.includes(key)) { + matches.push(value); + } + } + if (matches.length > 0) { + return { content: [{ type: 'text' as const, text: matches.join('\n\n---\n\n') }] }; + } + + // Word-level fuzzy — match any word + const words = query.split(/\s+/); + for (const [key, value] of concepts) { + if (words.some(w => key.includes(w))) { + matches.push(value); + } + } + if (matches.length > 0) { + return { content: [{ type: 'text' as const, text: `No exact match for "${concept}". Related concepts:\n\n${matches.slice(0, 5).join('\n\n---\n\n')}` }] }; + } + + return { content: [{ type: 'text' as const, text: `No concept matching "${concept}" found. ${concepts.size} concepts indexed from protocol terminology. Try broader terms like "channel", "state", "session", "escrow", "transfer".` }] }; + }, +); + +server.tool( + 'lookup_rpc_method', + 'Look up a v1 RPC method from docs/api.yaml — returns description, request/response fields. Methods use grouped naming: {group}.v1.{method}', + { method: z.string().describe('V1 RPC method name or search term (e.g. "channels.v1.get_home_channel", "submit_state", "get_balances")') }, + async ({ method }) => { + const query = method.toLowerCase().trim(); + + // Direct match + const doc = rpcMethodDocs.get(query); + if (doc) { + let text = `## V1 RPC Method: \`${doc.method}\`\n\n`; + text += `**Group:** ${doc.group}\n**Description:** ${doc.description}\n\n`; + text += `**Request fields:** ${doc.requestFields}\n`; + text += `**Response fields:** ${doc.responseFields}\n`; + return { content: [{ type: 'text' as const, text }] }; + } + + // Fuzzy match — search in full method name and short name + const matches: RPCMethodDoc[] = []; + for (const [key, val] of rpcMethodDocs) { + const shortName = key.split('.').pop() || ''; + if (key.includes(query) || query.includes(shortName) || shortName.includes(query)) { + matches.push(val); + } + } + if (matches.length > 0) { + const text = matches.map(d => + `- \`${d.method}\` — ${d.description}` + ).join('\n'); + return { content: [{ type: 'text' as const, text: `Matching v1 RPC methods:\n\n${text}` }] }; + } + + return { content: [{ type: 'text' as const, text: `No v1 RPC method matching "${method}". Available methods:\n${[...rpcMethodDocs.keys()].join(', ')}` }] }; + }, +); + +server.tool( + 'scaffold_project', + 'Generate a starter project structure for a new Nitrolite app — TypeScript or Go templates', + { template: z.enum(['transfer-app', 'app-session', 'ai-agent', 'go-transfer-app', 'go-app-session', 'go-ai-agent']).describe('Project template: TypeScript (transfer-app, app-session, ai-agent) or Go (go-transfer-app, go-app-session, go-ai-agent)') }, + async ({ template }) => { + // Go templates — different output shape + if (template === 'go-transfer-app' || template === 'go-app-session' || template === 'go-ai-agent') { + const goTemplateMap: Record = { + 'go-transfer-app': GO_SCAFFOLD_TRANSFER, + 'go-app-session': GO_SCAFFOLD_APP_SESSION, + 'go-ai-agent': GO_SCAFFOLD_AI_AGENT, + }; + const baseName = template.replace('go-', ''); + const goMod = `module my-nitrolite-${baseName}\n\ngo 1.25.0\n\nrequire (\n\t${GO_MODULE_PATH} ${GO_MODULE_VERSION}\n\tgithub.com/shopspring/decimal v1.4.0\n)`; + const envKey = template === 'go-ai-agent' ? 'AGENT_PRIVATE_KEY' : 'PRIVATE_KEY'; + const envExtra = template === 'go-transfer-app' ? '\nRECIPIENT=your_recipient_address' : template === 'go-app-session' ? '\nPEER_ADDRESS=peer_wallet_address' : ''; + const text = `# Scaffold: ${template}\n\n## go.mod\n\`\`\`\n${goMod}\n\`\`\`\n\n## main.go\n\`\`\`go\n${goTemplateMap[template]}\`\`\`\n\n## .env.example\n\`\`\`\n${envKey}=your_hex_key\nCLEARNODE_URL=wss://clearnode.example.com/ws\nRPC_URL=https://rpc.sepolia.org${envExtra}\n\`\`\`\n\n## Setup\n\`\`\`bash\ngo mod tidy\ngo run .\n\`\`\``; + return { content: [{ type: 'text' as const, text }] }; + } + const packageJson = { + name: `nitrolite-${template}`, + version: '0.1.0', + private: true, + type: 'module', + scripts: { start: 'npx tsx src/index.ts', build: 'tsc', typecheck: 'tsc --noEmit' }, + dependencies: { + '@yellow-org/sdk': '^1.2.0', + 'decimal.js': '^10.4.0', + viem: '^2.46.0', + }, + devDependencies: { typescript: '^5.7.0', tsx: '^4.19.0', '@types/node': '^22.0.0' }, + }; + + const tsconfig = { + compilerOptions: { + target: 'es2020', module: 'ESNext', moduleResolution: 'bundler', + strict: true, esModuleInterop: true, outDir: 'dist', declaration: true, + }, + include: ['src'], + }; + + const templates: Record = { + 'transfer-app': `import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\$\{string}\`; +const CLEARNODE_URL = process.env.CLEARNODE_URL || 'wss://clearnode.example.com/ws'; +const RPC_URL = process.env.RPC_URL || 'https://rpc.sepolia.org'; +const CHAIN_ID = 80002n; + +async function main() { + const { stateSigner, txSigner } = createSigners(PRIVATE_KEY); + + const client = await Client.create(CLEARNODE_URL, stateSigner, txSigner, withBlockchainRPC(CHAIN_ID, RPC_URL)); + console.log('Connected to clearnode'); + + // Deposit funds + await client.deposit(CHAIN_ID, 'usdc', new Decimal(10)); + await client.checkpoint('usdc'); + console.log('Deposited 10 USDC'); + + // Transfer + const recipient = process.env.RECIPIENT as \`0x\$\{string}\`; + await client.transfer(recipient, 'usdc', new Decimal(5)); + console.log('Transferred 5 USDC to', recipient); +} + +main().catch(console.error); +`, + 'app-session': `import { Client, createSigners, withBlockchainRPC, app } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\$\{string}\`; +const CLEARNODE_URL = process.env.CLEARNODE_URL || 'wss://clearnode.example.com/ws'; +const RPC_URL = process.env.RPC_URL || 'https://rpc.sepolia.org'; +const CHAIN_ID = 80002n; +const PEER = process.env.PEER_ADDRESS as \`0x\$\{string}\`; + +async function main() { + const { stateSigner, txSigner } = createSigners(PRIVATE_KEY); + const myAddress = stateSigner.address; + + const client = await Client.create(CLEARNODE_URL, stateSigner, txSigner, withBlockchainRPC(CHAIN_ID, RPC_URL)); + + // Fund the home channel before moving funds into the app session + await client.deposit(CHAIN_ID, 'usdc', new Decimal(20)); + await client.checkpoint('usdc'); + + // Define app session + const definition: app.AppDefinitionV1 = { + applicationId: 'my-app', + participants: [ + { walletAddress: myAddress, signatureWeight: 50 }, + { walletAddress: PEER, signatureWeight: 50 }, + ], + quorum: 100, + nonce: BigInt(Date.now()), + }; + + // Collect quorum signatures from participants (off-band) + const quorumSigs: string[] = ['0xMySig...', '0xPeerSig...']; + + // Create app session + const session = await client.createAppSession(definition, '{}', quorumSigs); + console.log('Session created:', session.appSessionId); + + // Fund the app session before non-zero allocations + const depositUpdate: app.AppStateUpdateV1 = { + appSessionId: session.appSessionId, + intent: app.AppStateUpdateIntent.Deposit, + version: 2n, + allocations: [ + { participant: myAddress, asset: 'usdc', amount: new Decimal(12) }, + { participant: PEER, asset: 'usdc', amount: new Decimal(8) }, + ], + sessionData: '{}', + }; + await client.submitAppSessionDeposit(depositUpdate, ['0xMySig...', '0xPeerSig...'], 'usdc', new Decimal(20)); + console.log('Session funded with 20 USDC'); + + // Submit state update + const update: app.AppStateUpdateV1 = { + appSessionId: session.appSessionId, + intent: app.AppStateUpdateIntent.Operate, + version: 3n, + allocations: depositUpdate.allocations, + sessionData: '{}', + }; + await client.submitAppState(update, ['0xMySig...', '0xPeerSig...']); + + // Close session — submit with Close intent + const closeUpdate: app.AppStateUpdateV1 = { ...update, intent: app.AppStateUpdateIntent.Close, version: 4n }; + await client.submitAppState(closeUpdate, ['0xMyCloseSig...', '0xPeerCloseSig...']); + console.log('Session closed'); +} + +main().catch(console.error); +`, + 'ai-agent': `import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +const AGENT_KEY = process.env.AGENT_PRIVATE_KEY as \`0x\$\{string}\`; +const CLEARNODE_URL = process.env.CLEARNODE_URL || 'wss://clearnode.example.com/ws'; +const RPC_URL = process.env.RPC_URL || 'https://rpc.sepolia.org'; +const CHAIN_ID = 80002n; + +async function createAgentClient() { + const { stateSigner, txSigner } = createSigners(AGENT_KEY); + return Client.create(CLEARNODE_URL, stateSigner, txSigner, withBlockchainRPC(CHAIN_ID, RPC_URL)); +} + +async function payForService(client: Awaited>, recipient: \`0x\$\{string}\`, amount: Decimal) { + const state = await client.transfer(recipient, 'usdc', amount); + console.log(\`Paid \$\{amount} USDC to \$\{recipient}, version: \$\{state.version}\`); + return state; +} + +async function main() { + const client = await createAgentClient(); + console.log('Agent connected to clearnode'); + + // Ensure the agent has funds + await client.deposit(CHAIN_ID, 'usdc', new Decimal(50)); + await client.checkpoint('usdc'); + + // Agent payment loop — pay for each task + const tasks = [ + { recipient: '0x1111111111111111111111111111111111111111' as \`0x\$\{string}\`, amount: new Decimal('0.10') }, + { recipient: '0x2222222222222222222222222222222222222222' as \`0x\$\{string}\`, amount: new Decimal('0.25') }, + ]; + + for (const task of tasks) { + await payForService(client, task.recipient, task.amount); + } + + console.log('All payments complete'); +} + +main().catch(console.error); +`, + }; + + const envExample = `${template === 'ai-agent' ? 'AGENT_PRIVATE_KEY' : 'PRIVATE_KEY'}=0x... +CLEARNODE_URL=wss://clearnode.example.com/ws +RPC_URL=https://rpc.sepolia.org +${template === 'transfer-app' ? 'RECIPIENT=0x...' : ''}${template === 'app-session' ? 'PEER_ADDRESS=0x...' : ''}`; + + const text = `# Scaffold: ${template} + +## package.json +\`\`\`json +${JSON.stringify(packageJson, null, 2)} +\`\`\` + +## tsconfig.json +\`\`\`json +${JSON.stringify(tsconfig, null, 2)} +\`\`\` + +## src/index.ts +\`\`\`typescript +${templates[template]} +\`\`\` + +## .env.example +\`\`\` +${envExample} +\`\`\` + +## Setup +\`\`\`bash +npm install +cp .env.example .env # Fill in your values +npx tsx src/index.ts +\`\`\``; + + return { content: [{ type: 'text' as const, text }] }; + }, +); + +// ========================== PROMPTS ======================================== + +server.prompt( + 'create-channel-app', + 'Step-by-step guide to build an app using Nitrolite state channels', + async () => ({ + messages: [{ + role: 'user' as const, + content: { + type: 'text' as const, + text: `Guide me through building a Nitrolite state channel application. Cover: + +1. **Setup** — Install dependencies (@yellow-org/sdk, viem), create Client with config +2. **Authentication** — Connect wallet, establish WebSocket, authenticate with clearnode +3. **Channel Lifecycle** — Deposit (auto-creates channel), query channels, close channel +4. **Transfers** — Send tokens to another participant via state channels +5. **App Sessions** — Fund the home channel with deposit + checkpoint, then create sessions, fund them with submitAppSessionDeposit, submit state, close +6. **Error Handling** — Common errors and how to handle them +7. **Testing** — How to write tests against the SDK + +For each step, show complete TypeScript code examples using the latest SDK API. +Use \`@yellow-org/sdk\` for new projects. Only use \`@yellow-org/sdk-compat\` if migrating from v0.5.3. + +## Go SDK + +Guide me through building a Nitrolite state channel application in Go. Cover: + +1. **Setup** — Install dependencies, create signers with sign.NewEthereumMsgSigner and sign.NewEthereumRawSigner +2. **Client Creation** — sdk.NewClient with functional options (WithBlockchainRPC, WithHandshakeTimeout) +3. **Channel Lifecycle** — Deposit (creates channel), Transfer, Checkpoint (on-chain), CloseHomeChannel + Checkpoint +4. **App Sessions** — Deposit + Checkpoint on the home channel, then CreateAppSession, SubmitAppSessionDeposit, SubmitAppState (Operate/Withdraw/Close intents) +5. **Error Handling** — Go error patterns, context.WithTimeout, defer client.Close() +6. **Testing** — Standard Go test patterns with *_test.go files + +For each step, show complete Go code examples using the latest SDK from github.com/layer-3/nitrolite/sdk/go. +Use github.com/shopspring/decimal for amounts. Use context.Context for all async operations.`, + }, + }], + }), +); + +server.prompt( + 'migrate-from-v053', + 'Interactive migration assistant from @layer-3/nitrolite v0.5.3 to the compat layer', + async () => { + const migrationDocs = readFile(resolve(COMPAT_ROOT, 'docs/migration-overview.md')); + return { + messages: [{ + role: 'user' as const, + content: { + type: 'text' as const, + text: `I need to migrate my app from \`@layer-3/nitrolite\` v0.5.3 to the new SDK. Help me step by step. + +Here is the official migration guide: + +${migrationDocs} + +Walk me through: +1. Installing \`@yellow-org/sdk-compat\` and peer deps +2. Swapping imports (package name change) +3. Replacing create-sign-send-parse pattern with NitroliteClient methods +4. Updating type references if any changed +5. Testing the migrated code + +Ask me to paste my current code so you can provide specific migration instructions.`, + }, + }], + }; + }, +); + +server.prompt( + 'build-ai-agent-app', + 'Guided conversation for building an AI agent that uses Nitrolite for payments', + async () => ({ + messages: [{ + role: 'user' as const, + content: { + type: 'text' as const, + text: `I want to build an AI agent that uses Nitrolite state channels for payments. Guide me through: + +1. **Agent Wallet Setup** — Create a wallet for the agent, configure the SDK client +2. **Channel Management** — Open a channel, deposit funds for the agent to use +3. **Automated Payments** — Implement a payment function the agent can call autonomously +4. **Session Key Delegation** — Set up a session key with spending caps for security +5. **Agent-to-Agent Payments** — Transfer funds between two autonomous agents +6. **Integration** — Wrap SDK methods as tools for an agent framework (LangChain, CrewAI, etc.) +7. **Error Handling** — Handle reconnection, insufficient funds, expired sessions + +For each step, show complete TypeScript code examples using the latest SDK API (\`@yellow-org/sdk\`). +Use \`viem\` for Ethereum interactions. Include proper error handling and logging. + +## Go SDK + +I want to build an AI agent in Go that uses Nitrolite state channels for payments. Guide me through: + +1. **Agent Wallet Setup** — Create signers from a private key, configure the SDK client +2. **Channel Management** — Open a channel, deposit funds for the agent +3. **Automated Payments** — A goroutine-safe payment function using context and mutexes +4. **Session Key Delegation** — Set up session keys with spending caps +5. **Agent-to-Agent Payments** — Transfer between two autonomous Go agents +6. **Graceful Shutdown** — Handle OS signals, defer client.Close(), WaitCh() +7. **Error Handling** — Wrapped errors, retry patterns, connection recovery + +For each step, show complete Go code using github.com/layer-3/nitrolite/sdk/go. +Use context.Context for timeouts. Use decimal.Decimal for amounts. Follow standard Go patterns.`, + }, + }], + }), +); + +// --------------------------------------------------------------------------- +// Start +// --------------------------------------------------------------------------- + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('Nitrolite SDK MCP server running on stdio'); +} + +main().catch((err) => { + console.error('Fatal:', err); + process.exit(1); +}); diff --git a/sdk/mcp/tsconfig.json b/sdk/mcp/tsconfig.json new file mode 100644 index 000000000..9ee88030f --- /dev/null +++ b/sdk/mcp/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true + }, + "include": ["src"] +} diff --git a/sdk/ts-compat/CLAUDE.md b/sdk/ts-compat/CLAUDE.md new file mode 100644 index 000000000..e618d4db4 --- /dev/null +++ b/sdk/ts-compat/CLAUDE.md @@ -0,0 +1,60 @@ +# TypeScript SDK Compat (`@yellow-org/sdk-compat`) + +Compatibility layer that bridges the old `@layer-3/nitrolite` v0.5.3 API to the `@yellow-org/sdk` v1.0.0+ runtime. Lets existing dApps upgrade with minimal code changes. + +## Quick Reference + +| Command | What it does | +|---------|-------------| +| `npm test` | Run unit tests (Jest) | +| `npm run build` | Compile with tsc | +| `npm run typecheck` | Type check only | + +## Package Details + +- **Name:** `@yellow-org/sdk-compat` +- **Version:** 1.2.1 +- **Peer deps:** `@yellow-org/sdk >=1.2.0`, `viem ^2.0.0` +- **Dev dep:** `"@yellow-org/sdk": "file:../ts"` — **must build `sdk/ts` first** + +## Critical Constraint: No Barrel Re-Export of SDK Classes + +The main SDK (`@yellow-org/sdk`) has side effects on module evaluation that break SSR. + +**DO NOT** add `export { Client } from '@yellow-org/sdk'` to `index.ts`. + +**SAFE:** `export type { StateSigner } from '@yellow-org/sdk'` (type-only exports are erased at compile time). + +This is documented in `src/index.ts` lines 154-158. + +## Source Layout + +| Path | Purpose | +|------|---------| +| `src/index.ts` | Barrel export — update when adding public API | +| `src/client.ts` | NitroliteClient facade (~1100 lines) | +| `src/auth.ts` | Auth helpers (createAuthRequestMessage, etc.) | +| `src/rpc.ts` | RPC compat stubs (create*Message / parse*Response) | +| `src/types.ts` | All types, enums, interfaces | +| `src/signers.ts` | WalletStateSigner, createECDSAMessageSigner | +| `src/app-signing.ts` | App session hash packing | +| `src/errors.ts` | CompatError class hierarchy | +| `src/events.ts` | EventPoller (polling bridge for v0.5.3 push events) | +| `src/config.ts` | Configuration builders | +| `docs/` | Migration guides (overview, on-chain, off-chain) | +| `test/unit/` | Unit tests | + +## Test Setup + +- Framework: Jest with ts-jest +- Config: `jest.config.cjs` +- ESM handling: `transformIgnorePatterns` whitelists `@yellow-org` for ts-jest +- Pattern: manual mock signers (`async () => '0xsig'`) + +## When Adding Exports + +1. Add the function/type to the appropriate source file +2. Export it from `src/index.ts` +3. If re-exporting from `@yellow-org/sdk`, use `export type` only (SSR safety) +4. Update `README.md` (Types Reference, RPC Stubs, or Auth Helpers section) +5. Add a test in `test/unit/exports.test.ts` diff --git a/sdk/ts/CLAUDE.md b/sdk/ts/CLAUDE.md new file mode 100644 index 000000000..cf5ffa0b2 --- /dev/null +++ b/sdk/ts/CLAUDE.md @@ -0,0 +1,67 @@ +# TypeScript SDK (`@yellow-org/sdk`) + +Official TypeScript SDK for building web and mobile applications on Nitrolite state channels. + +## Quick Reference + +| Command | What it does | +|---------|-------------| +| `npm test` | Run unit tests (Jest) | +| `npm run build` | Run tests **then** compile (`npm run test && tsc`) | +| `npm run typecheck` | Type check only (no emit) | +| `npm run lint` | ESLint | +| `npm run test:integration` | Integration tests (separate Jest config) | +| `npm run test:all` | Unit + integration tests | + +**Important:** `npm run build` runs the full test suite before compiling. If you just want to compile, run `npx tsc` directly. + +## Package Details + +- **Name:** `@yellow-org/sdk` +- **Version:** 1.2.0 +- **Module:** ESM-only (`"type": "module"`) +- **Node:** >=20.0.0 +- **Entry:** `dist/index.js` / `dist/index.d.ts` + +## TypeScript Configuration + +- Target: `es2020` +- Module: `ESNext` +- Module Resolution: `bundler` +- Strict mode: enabled +- Declaration files: generated + +## Code Style + +- Prettier: 120 char width, 4-space indent, single quotes, semicolons +- ESLint configured via `@typescript-eslint` + +## Source Layout + +| Path | Purpose | +|------|---------| +| `src/index.ts` | Barrel export — all public API goes here | +| `src/client.ts` | Main SDK client (~1100 lines) | +| `src/signers.ts` | Signer implementations (ECDSA, session key, channel) | +| `src/config.ts` | Client configuration with functional options | +| `src/core/` | State machine, types, events, utilities | +| `src/rpc/` | RPC client, message types, API methods | +| `src/blockchain/` | On-chain interactions (deposits, withdrawals) | +| `src/app/` | App session logic and packing | +| `src/utils.ts` | Shared utility functions | +| `test/unit/` | Unit tests | + +## Test Setup + +- Framework: Jest with ts-jest +- Config: `jest.config.cjs` (unit), `jest.integration.config.js` (integration) +- Path alias: `@/` maps to `src/` in test configs +- Pattern: data-driven tests with `.forEach()`, manual mocks (not jest.mock) +- Naming: `*.test.ts` (not `.spec.ts`) + +## Key Dependencies + +- `viem` — Ethereum interactions (NOT ethers.js in production code; ethers is dev-only) +- `decimal.js` — Precise decimal arithmetic for token amounts +- `zod` — Runtime validation +- `abitype` — ABI type utilities