diff --git a/.github/workflows/ts-ci.yml b/.github/workflows/ts-ci.yml index 7e0a1a810..79404c2c1 100644 --- a/.github/workflows/ts-ci.yml +++ b/.github/workflows/ts-ci.yml @@ -32,6 +32,7 @@ jobs: node: - "22" - "24" + - "26" defaults: run: @@ -45,6 +46,8 @@ jobs: uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} + # The runner image can lag behind the security releases that `engines` requires. + check-latest: true cache: npm cache-dependency-path: ts/package-lock.json diff --git a/.github/workflows/ts-standalone-release.yml b/.github/workflows/ts-standalone-release.yml index f79f372c2..d0fd65b08 100644 --- a/.github/workflows/ts-standalone-release.yml +++ b/.github/workflows/ts-standalone-release.yml @@ -11,7 +11,7 @@ permissions: contents: read env: - NODE_VERSION: "20" + NODE_VERSION: "22" jobs: quality: @@ -29,6 +29,8 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} + # The runner image can lag behind the security releases that `engines` requires. + check-latest: true cache: npm cache-dependency-path: ts/package-lock.json @@ -94,6 +96,8 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} + # The runner image can lag behind the security releases that `engines` requires. + check-latest: true cache: npm cache-dependency-path: ts/package-lock.json diff --git a/CLAUDE.md b/CLAUDE.md index c5c4d3539..dc63e814f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ Everything below (except the TypeScript Implementation section) refers to the Ja ## TypeScript Implementation -The `ts/` package is a self-contained, agent-first CLI (Node.js 20+, ESM, TypeScript). Every command +The `ts/` package is a self-contained, agent-first CLI (Node.js 22.23.4+ / 24.22.1+ / 26.12.1+, ESM, TypeScript). Every command has a stable JSON envelope, deterministic exit codes, and discoverable schemas; interactive prompts are used only for secret input (create / import / backup / delete). **All `ts/` commands run from the `ts/` directory.** diff --git a/ts/README.md b/ts/README.md index efcf8d8ef..35693e8ff 100644 --- a/ts/README.md +++ b/ts/README.md @@ -48,7 +48,7 @@ The TRON ids used before CAIP-2 (`tron:mainnet`, `tron:nile`, `tron:shasta`) rem ## Install -**Prerequisites**: [Node.js](https://nodejs.org) **20 or later** (`node --version` to check). Ledger signing additionally needs a supported Ledger device with the TRON or Ethereum app installed — see the [Ledger guide](docs/guide/ledger.md). +**Prerequisites**: [Node.js](https://nodejs.org) **22.23.4+, 24.22.1+ or 26.12.1+** — the October 2026 security releases (`node --version` to check). Ledger signing additionally needs a supported Ledger device with the TRON or Ethereum app installed — see the [Ledger guide](docs/guide/ledger.md). ```bash npm install -g @tron-walletcli/wallet-cli diff --git a/ts/package-lock.json b/ts/package-lock.json index f354a078b..da3ba39a6 100644 --- a/ts/package-lock.json +++ b/ts/package-lock.json @@ -65,7 +65,7 @@ "vitest": "^4.1.9" }, "engines": { - "node": ">=20" + "node": ">=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1" } }, "node_modules/@adraffy/ens-normalize": { diff --git a/ts/package.json b/ts/package.json index 3970afe65..006b5d0f6 100644 --- a/ts/package.json +++ b/ts/package.json @@ -41,7 +41,7 @@ "access": "public" }, "engines": { - "node": ">=20" + "node": ">=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1" }, "scripts": { "build": "tsup", diff --git a/ts/src/adapters/outbound/x402/server.test.ts b/ts/src/adapters/outbound/x402/server.test.ts index 8e971ffd8..b950aaecb 100644 --- a/ts/src/adapters/outbound/x402/server.test.ts +++ b/ts/src/adapters/outbound/x402/server.test.ts @@ -64,6 +64,24 @@ it("rejects malformed request URLs and keeps serving health requests", async () }); }); +it("closes the connection after a 402 challenge so the paid retry cannot reuse a stale socket", async () => { + await withServer({}, async (port) => { + const response = await new Promise<{ status?: number; connection?: string }>( + (resolve, reject) => { + const req = request({ hostname: "127.0.0.1", port, path: "/pay" }, (res) => { + res.resume(); + res.on("end", () => + resolve({ status: res.statusCode, connection: res.headers.connection }), + ); + }); + req.on("error", reject); + req.end(); + }, + ); + expect(response).toEqual({ status: 402, connection: "close" }); + }); +}); + it.each([ [{ success: true, transaction: "", network: "tron:0xcd8690dc" }, 502], [{ success: true, transaction: "a".repeat(64), network: "eip155:1" }, 502], diff --git a/ts/src/adapters/outbound/x402/server.ts b/ts/src/adapters/outbound/x402/server.ts index 161a78282..1f4d1322a 100644 --- a/ts/src/adapters/outbound/x402/server.ts +++ b/ts/src/adapters/outbound/x402/server.ts @@ -200,6 +200,10 @@ export class X402HttpServer implements X402ServerPort { if (pathname !== "/pay") return json(response, 404, { error: "not found" }); const signature = request.headers["payment-signature"]; if (!signature || Array.isArray(signature)) { + // The paid retry follows key decryption and signing, which can block the client's event + // loop past keepAliveTimeout (roundtrip runs both ends in one process). A reused socket + // would then be reset under the retry, so make the client open a fresh one. + response.setHeader("connection", "close"); response.setHeader("payment-required", encodePaymentRequiredHeader(challenge as never)); return json(response, 402, challenge); } diff --git a/ts/test/bai-recharge.test.ts b/ts/test/bai-recharge.test.ts index a9545b969..c03931896 100644 --- a/ts/test/bai-recharge.test.ts +++ b/ts/test/bai-recharge.test.ts @@ -144,5 +144,5 @@ it.each([ rmSync(home, { recursive: true, force: true }); } }, - 60000, + 120000, ); diff --git a/ts/test/beta-server-roundtrip.test.ts b/ts/test/beta-server-roundtrip.test.ts index d41dfe667..4967db568 100644 --- a/ts/test/beta-server-roundtrip.test.ts +++ b/ts/test/beta-server-roundtrip.test.ts @@ -138,7 +138,7 @@ import {appendFileSync} from 'node:fs';const realFetch=globalThis.fetch;globalTh env: { ...process.env, WALLET_CLI_HOME: home }, input: "test-password\n", encoding: "utf8", - timeout: 20000, + timeout: 40000, }, ); expect(r.status, r.stderr + r.stdout).toBe(0); @@ -159,7 +159,7 @@ import {appendFileSync} from 'node:fs';const realFetch=globalThis.fetch;globalTh rmSync(home, { recursive: true, force: true }); } }, - 30000, + 60000, ); it @@ -262,7 +262,7 @@ globalThis.fetch = async(input,init) => { env: { ...process.env, WALLET_CLI_HOME: home }, input: "test-password\n", encoding: "utf8", - timeout: 20000, + timeout: 40000, }, ); if (missing) { @@ -296,7 +296,7 @@ globalThis.fetch = async(input,init) => { rmSync(home, { recursive: true, force: true }); } }, - 30000, + 60000, ); it.skipIf(!entry).each(["0", "0.000", "0.5", "9007199254740992"])( diff --git a/ts/vitest.config.ts b/ts/vitest.config.ts index 46871caa4..f9a0a5276 100644 --- a/ts/vitest.config.ts +++ b/ts/vitest.config.ts @@ -30,7 +30,9 @@ export default defineConfig({ sequence: { groupOrder: 1 }, environment: "node", include: ["test/**/*.test.ts"], - testTimeout: 30_000, + // Cases that spawn the CLI several times with real scrypt have measured ~30s on a loaded + // 4-vCPU CI runner, so 30s left no headroom. + testTimeout: 60_000, hookTimeout: 30_000, // Build once, then every case spawns `node dist/index.js` instead of cold-transpiling // the whole CLI through tsx. `verify:package` sets WALLET_CLI_TEST_ENTRY to the