diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 070e85d..7ebba6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,6 +200,19 @@ jobs: } process.exit(1); } + // Subpath exports must also resolve from the tarball — 2.7.x shipped + // without ai-path/ in "files" or "./ai-path" in "exports", breaking + // every AI-agent consumer (ERR_PACKAGE_PATH_NOT_EXPORTED). + for (const sub of ['blue-js-sdk/ai-path', 'blue-js-sdk/consumer', 'blue-js-sdk/operator']) { + try { + const m = await import(sub); + console.log(`Subpath OK: ${sub} (${Object.keys(m).length} exports)`); + } catch (e) { + console.error(`FAILED to import subpath ${sub}:`, e.message); + console.error('Check package.json "exports" map AND "files" array — both must include it.'); + process.exit(1); + } + } SCRIPT - name: Verify submodule imports resolve diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dfeaaf..160be4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,51 @@ Every fix made during SDK creation, why it matters, and what happens if you use upstream Sentinel code directly without these fixes. +## v2.8.0 — Restore `ai-path` packaging + fail-fast auto-install in `connect()` (2026-06-12) + +Two regressions, one failure class, found by an AI-agent consumer (x402 pay-per-use +VPN flow) running on a machine with no tunnel binary installed. + +**1. Published 2.7.2 shipped without `ai-path` entirely.** A package.json revert +(commit `f6162f4`) dropped `"./ai-path"` from `exports`, `ai-path/` from `files`, +and the `sentinel-ai` bin. Because an `exports` map is present, every npm consumer +of `import 'blue-js-sdk/ai-path'` threw `ERR_PACKAGE_PATH_NOT_EXPORTED` — the exact +failure class v2.7.2's CI gate was built for, except that gate only imported the +package ROOT, so subpath drift slipped through. Only apps pinned to 2.3.0 kept working. + +**2. A binary-less agent burned a real on-chain session.** `connect()` step 1 +detected the environment but never gated on it: wallet, balance check, node +selection, and the MsgStartSession TX all ran, then the tunnel step failed with +`V2RAY_NOT_FOUND` — wasting the session payment and the fee granter's allowance. + +### Fix +- `package.json`: restored `"./ai-path": "./ai-path/index.js"` in `exports`, + `ai-path/` in `files`, `"sentinel-ai": "ai-path/cli.js"` in `bin`. Version 2.8.0. +- `ai-path/connect.js`: step 1 now GATES — no V2Ray and no admin-usable WireGuard + → auto-download V2Ray (no admin needed); if still unusable, throw + `ENVIRONMENT_NOT_READY` (nextAction `run_setup`) before any chain work. + Skipped in `dryRun` mode. +- `ai-path/environment.js`: `setup()` now actually installs — when no usable + tunnel binary exists it downloads V2Ray (SHA256-verified) via the root setup + script, re-detects, and reports `installed: true`. Opt out with + `{ autoInstall: false }`. +- `setup.js` (root): now safe to import — `setupV2Ray()`, `setupWireGuard()`, and + a combined `setup()` are exported; the interactive `main()` only runs when the + file is executed directly (it previously ran npm-install/MSI side effects on + import). +- `.github/workflows/ci.yml`: the tarball gate now also imports every subpath + export (`blue-js-sdk/ai-path`, `/consumer`, `/operator`) from the packed install. +- Docs: `ai-path/README.md` (setup() API, `ENVIRONMENT_NOT_READY`, lifecycle), + `ai-path/FAILURES.md` (D8, D9). + +### Rule +**Every subpath in `exports` must be import-tested from the packed tarball in CI** +— a root-only import check cannot see subpath regressions. And **detection without +a gate is decoration**: every prerequisite verified at step 1 must abort before +the first token is spent. + +--- + ## v2.7.2 — Packaging Fix: include `auth/` and `operator/` in tarball (2026-05-02) **2.7.1 shipped without `auth/` and `operator/` directories.** `index.js` imports diff --git a/ai-path/FAILURES.md b/ai-path/FAILURES.md index 6f98ce4..a790026 100644 --- a/ai-path/FAILURES.md +++ b/ai-path/FAILURES.md @@ -173,6 +173,8 @@ | D5 | Git Bash mangles `/F` flag | `taskkill /F /PID 32516` fails with "Invalid argument" | Git Bash converts `/F` to `F:/` (POSIX path conversion) | Use `//F` or `execFileSync` (bypasses shell) | Use `execFileSync` for all system commands, never string interpolation | | D6 | Competing VPN applications | WireGuard tunnel fails; routing table conflicts | NordVPN/ExpressVPN/etc. have active tunnels, route overrides, port conflicts | Added VPN conflict detection in pre-connect diagnostic | Detect and warn about competing VPNs before connecting | | D7 | WireGuard Manager Service ghost | WireGuard GUI takes over tunnel management; conflicts with programmatic control | `wireguard.exe /installmanagerservice` was called instead of `/installtunnelservice` | Never call `/installmanagerservice`; only use `/installtunnelservice` | SDK must only use direct tunnel service management | +| D8 | Binary-less `connect()` burns an on-chain session | Agent with no V2Ray/WireGuard ran connect(): wallet, balance, node selection, and a real MsgStartSession TX all succeeded — then the tunnel step failed with V2RAY_NOT_FOUND. Session payment + fee-granter allowance wasted | Step 1 detected the environment but never GATED on it; nothing between detection and the on-chain TX checked for a usable tunnel binary | connect() now gates at step 1: no V2Ray and no admin-usable WireGuard → auto-download V2Ray (no admin needed); if still unusable, throw `ENVIRONMENT_NOT_READY` (nextAction `run_setup`) BEFORE any chain work | Detection without a gate is decoration. Every prerequisite checked at step 1 must abort BEFORE the first token is spent | +| D9 | Published 2.7.2 tarball missing `ai-path/` entirely | Every npm consumer of `blue-js-sdk/ai-path` got ERR_PACKAGE_PATH_NOT_EXPORTED; `sentinel-ai` CLI gone. Only apps pinned to 2.3.0 kept working | A later commit accidentally reverted package.json, dropping `./ai-path` from `exports`, `ai-path/` from `files`, and the `sentinel-ai` bin. CI's tarball check only imported the package root, so subpaths slipped through | Restored all three entries (2.8.0); CI tarball gate now imports every subpath export (`./ai-path`, `./consumer`, `./operator`) from a real packed install | Every subpath in `exports` must be import-tested from the packed tarball in CI — root-only import checks miss subpath regressions | ### TESTING diff --git a/ai-path/README.md b/ai-path/README.md index fccd234..9fc9a66 100644 --- a/ai-path/README.md +++ b/ai-path/README.md @@ -47,6 +47,8 @@ Post-install runs `setup.js` automatically to download V2Ray 5.2.1. If it fails npx sentinel-ai setup ``` +If the install ran with `--ignore-scripts` (common in CI and agent sandboxes), nothing is lost: both `setup()` and `connect()` detect the missing binary at runtime and auto-download V2Ray before any tokens are spent. + ### Requirements | Requirement | Details | @@ -325,9 +327,19 @@ const bal = await getBalance(process.env.MNEMONIC); console.log(`${bal.p2p} (${bal.udvpn} udvpn) — funded: ${bal.funded}`); ``` -### `setup()` -> `{ ready, environment, preflight, issues }` +### `setup(opts?)` -> `{ ready, v2ray, wireguard, admin, installed, capabilities, preflight, issues, ... }` + +Verifies dependencies (V2Ray binary, WireGuard, Node.js version) and tests chain reachability. **If no usable tunnel binary exists** (no V2Ray, and WireGuard either missing or unusable without admin), `setup()` **auto-downloads V2Ray** to the SDK's `bin/` directory — no admin rights needed, SHA256-verified. Pass `{ autoInstall: false }` to disable. + +Returns a flat structure: `ready: true` when a connection is possible right now. `installed: true` when this call downloaded V2Ray. Check `issues: string[]` for anything missing. The `environment` field still carries the nested `getEnvironment()` data for backward compatibility. + +```js +import { setup } from 'blue-js-sdk/ai-path'; +const env = await setup(); // downloads V2Ray if nothing usable is present +console.log(env.ready, env.v2ray, env.installed); +``` -Verifies dependencies (V2Ray binary, WireGuard, Node.js version) and tests chain reachability. Returns `ready: true` if all required dependencies are present. Check `issues: string[]` for what is missing. The `environment` field contains the same data as `getEnvironment()`. +`connect()` runs the same gate automatically at step 1: missing binary → auto-install → if still unusable, throws `ENVIRONMENT_NOT_READY` **before any tokens are spent**. ### `discoverNodes(opts?)` -> `Node[]` @@ -408,6 +420,7 @@ try { | `SESSION_EXTRACT_FAILED` | recoverable | TX succeeded but session ID extraction failed | | `PARTIAL_CONNECTION_FAILED` | recoverable | Payment succeeded, tunnel failed. Session is on-chain. | | `V2RAY_NOT_FOUND` | infrastructure | V2Ray binary not found. Run `setup()`. | +| `ENVIRONMENT_NOT_READY` | infrastructure | No usable tunnel binary and auto-install failed. Thrown at step 1, BEFORE any tokens are spent. Run `setup()`. | | `WG_NOT_AVAILABLE` | infrastructure | WireGuard not installed | | `TLS_CERT_CHANGED` | infrastructure | Node TLS certificate changed unexpectedly | | `SESSION_POISONED` | fatal | Session previously failed. Start a new one. | @@ -503,7 +516,7 @@ blue-agent-connect ### Connection Lifecycle ``` -1. SETUP Download V2Ray, verify dependencies +1. SETUP Detect environment; auto-download V2Ray if no usable tunnel binary (gates BEFORE payment) 2. WALLET Derive keypair from mnemonic 3. DISCOVER Query blockchain for online nodes with P2P pricing 4. SELECT Pick best node (by country, price, protocol, or auto) diff --git a/ai-path/connect.js b/ai-path/connect.js index 71a60b1..24c5885 100644 --- a/ai-path/connect.js +++ b/ai-path/connect.js @@ -181,6 +181,10 @@ function humanError(err) { message: 'V2Ray binary not found. Run setup first: node setup.js', nextAction: 'run_setup', }, + ENVIRONMENT_NOT_READY: { + message: 'No usable tunnel binary. Run setup() from blue-js-sdk/ai-path — it auto-downloads V2Ray (no admin needed).', + nextAction: 'run_setup', + }, HANDSHAKE_FAILED: { message: 'Handshake with node failed. The node may be overloaded — try another.', nextAction: 'try_different_node', @@ -344,6 +348,33 @@ export async function connect(opts = {}) { } catch { /* environment detection failed */ } log(1, totalSteps, 'ENVIRONMENT', `OS=${envInfo.os} | admin=${envInfo.admin} | v2ray=${envInfo.v2ray} | wireguard=${envInfo.wireguard}`); + + // Gate HERE, before any wallet/balance/on-chain work. Without this guard a + // binary-less agent burns a real MsgStartSession TX (and the fee granter's + // allowance) only to fail at the tunnel step with V2RAY_NOT_FOUND. + // WireGuard without admin cannot connect either, so that case also gates. + const wgUsable = envInfo.wireguard && envInfo.admin; + if (!envInfo.v2ray && !wgUsable && opts.dryRun !== true) { + log(1, totalSteps, 'ENVIRONMENT', 'No usable tunnel binary — auto-installing V2Ray (no admin needed)...'); + try { + const { setup } = await import('./environment.js'); + const result = await setup(); // downloads V2Ray when missing, re-detects + envInfo.v2ray = result.v2ray; + envInfo.v2rayPath = result.v2rayPath; + envInfo.wireguard = result.wireguard; + envInfo.admin = result.admin; + } catch (err) { + log(1, totalSteps, 'ENVIRONMENT', `Auto-setup failed: ${err.message}`); + } + if (!envInfo.v2ray && !(envInfo.wireguard && envInfo.admin)) { + const err = new Error('No usable tunnel binary (V2Ray missing; WireGuard missing or needs admin). Run setup() from blue-js-sdk/ai-path, or: node node_modules/blue-js-sdk/setup.js. No tokens were spent.'); + err.code = 'ENVIRONMENT_NOT_READY'; + err.nextAction = 'run_setup'; + err.details = { v2ray: envInfo.v2ray, wireguard: envInfo.wireguard, admin: envInfo.admin }; + throw err; + } + log(1, totalSteps, 'ENVIRONMENT', `Tunnel binary ready: v2ray=${envInfo.v2ray}${envInfo.v2rayPath ? ` (${envInfo.v2rayPath})` : ''}`); + } timings.environment = Date.now() - t0; // ── STEP 2/7: Wallet ────────────────────────────────────────────────────── diff --git a/ai-path/environment.js b/ai-path/environment.js index bc14f98..8dba94e 100644 --- a/ai-path/environment.js +++ b/ai-path/environment.js @@ -15,7 +15,7 @@ import { import { existsSync } from 'fs'; import { execSync } from 'child_process'; import { resolve, dirname } from 'path'; -import { fileURLToPath } from 'url'; +import { fileURLToPath, pathToFileURL } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -102,6 +102,26 @@ function findWireGuard() { return null; } +// ─── V2Ray Auto-Install ────────────────────────────────────────────────────── + +/** + * Download V2Ray via the parent SDK's setup script (no admin rights needed). + * Downloads the official v2fly release zip, verifies SHA256, unzips to the + * SDK's bin/ directory — the same location findV2Ray() checks first. + * Throws with context when the setup script is missing or the download fails. + */ +async function installV2Ray() { + const parentSetup = resolve(__dirname, '..', 'setup.js'); + if (!existsSync(parentSetup)) { + throw new Error(`SDK setup script not found at ${parentSetup} — reinstall blue-js-sdk`); + } + const mod = await import(pathToFileURL(parentSetup).href); + if (typeof mod.setupV2Ray !== 'function') { + throw new Error('SDK setup script does not export setupV2Ray() — blue-js-sdk version too old (need >= 2.8.0)'); + } + await mod.setupV2Ray(); +} + // ─── getEnvironment() ──────────────────────────────────────────────────────── /** @@ -192,9 +212,15 @@ export function getEnvironment() { * Full environment setup: check deps, install missing ones, report status. * Runs preflight checks that verify everything needed for a VPN connection. * + * When no usable tunnel protocol is detected (no V2Ray, and no WireGuard + * usable without admin), setup() auto-downloads V2Ray to the SDK's bin/ + * directory — no admin rights needed. Pass { autoInstall: false } to skip. + * * Returns a FLAT structure — agents access .os, .v2ray, .wireguard directly. * No nested .environment wrapper to misread. * + * @param {object} [opts] + * @param {boolean} [opts.autoInstall=true] - Download V2Ray when no usable tunnel binary exists * @returns {Promise<{ * ready: boolean, * os: string, @@ -207,15 +233,35 @@ export function getEnvironment() { * v2rayPath: string|null, * wireguard: boolean, * wireguardPath: string|null, + * installed: boolean, * capabilities: string[], * recommended: string[], * preflight: object|null, * issues: string[], * }>} */ -export async function setup() { - const env = getEnvironment(); +export async function setup(opts = {}) { + const autoInstall = opts.autoInstall !== false; + let env = getEnvironment(); const issues = []; + let installed = false; + + // Auto-install V2Ray when nothing usable is present. 'wireguard' only + // appears in capabilities when the binary exists AND we have admin — a + // non-admin WireGuard-only machine still cannot connect, so V2Ray + // (which needs no admin) is downloaded for that case too. + if (autoInstall && !env.v2ray.available && !env.capabilities.includes('wireguard')) { + try { + await installV2Ray(); + env = getEnvironment(); // re-detect — bin/ now holds v2ray + geoip/geosite + installed = env.v2ray.available; + if (!installed) { + issues.push('V2Ray download completed but binary still not detected — check SDK bin/ directory permissions'); + } + } catch (err) { + issues.push(`V2Ray auto-install failed: ${err.message}`); + } + } // Run preflight checks — pass already-detected V2Ray path to avoid contradiction (BUG-1 fix) let preflightResult = null; @@ -234,7 +280,7 @@ export async function setup() { } if (!env.v2ray.available && !env.wireguard.available) { - issues.push('No tunnel protocol available — install V2Ray or WireGuard'); + issues.push('No tunnel protocol available — install V2Ray or WireGuard (re-run setup() or: node node_modules/blue-js-sdk/setup.js)'); } if (env.v2ray.available && env.v2ray.version && env.v2ray.version !== V2RAY_VERSION) { @@ -256,6 +302,7 @@ export async function setup() { v2rayPath: env.v2ray.path, wireguard: env.wireguard.available, wireguardPath: env.wireguard.path, + installed, capabilities: env.capabilities, recommended: env.recommended, preflight: preflightResult, diff --git a/package.json b/package.json index 9180b31..ed04f90 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "blue-js-sdk", - "version": "2.7.2", + "version": "2.8.0", "description": "Decentralized VPN SDK for the Sentinel P2P bandwidth network. WireGuard + V2Ray tunnels, Cosmos blockchain, 900+ nodes. Tested on Windows. macOS/Linux support included but untested.", "type": "module", "main": "index.js", "types": "types/index.d.ts", "bin": { - "sentinel": "cli/index.js" + "sentinel": "cli/index.js", + "sentinel-ai": "ai-path/cli.js" }, "exports": { ".": { @@ -15,6 +16,7 @@ }, "./consumer": "./consumer.js", "./operator": "./operator.js", + "./ai-path": "./ai-path/index.js", "./blue": { "types": "./dist/index.d.ts", "default": "./dist/index.js" @@ -49,6 +51,7 @@ "errors/", "examples/", "docs/", + "ai-path/", "bin/setup.js", "dist/", "src/", diff --git a/setup.js b/setup.js index 9fbfee4..04a508b 100644 --- a/setup.js +++ b/setup.js @@ -93,7 +93,7 @@ function unzip(zipPath, destDir) { // ─── V2Ray ────────────────────────────────────────────────────────────────── -async function setupV2Ray() { +export async function setupV2Ray() { const v2rayPath = path.join(BIN_DIR, V2RAY_BINARY); // Check if already exists @@ -230,7 +230,7 @@ function isAdmin() { return process.getuid?.() === 0; } -async function setupWireGuard() { +export async function setupWireGuard() { const existing = findWireGuard(); if (existing) { ok(`WireGuard found at ${existing}`); @@ -349,15 +349,23 @@ function checkDeps() { // ─── Main ─────────────────────────────────────────────────────────────────── +/** + * Full setup: npm deps + V2Ray download + WireGuard install/instructions. + * Importable — throws on failure instead of exiting the process. + */ +export async function setup() { + checkDeps(); + await setupV2Ray(); + await setupWireGuard(); +} + async function main() { console.log(''); console.log(' Sentinel dVPN SDK — Setup'); console.log(' ─────────────────────────'); console.log(''); - checkDeps(); - await setupV2Ray(); - await setupWireGuard(); + await setup(); console.log(''); console.log(' Setup complete! Quick start:'); @@ -370,7 +378,15 @@ async function main() { console.log(''); } -main().catch(err => { - console.error(`[setup] Fatal: ${err.message}`); - process.exit(1); -}); +// Run main() only when executed directly (node setup.js) — NOT on import. +// ai-path/environment.js imports setupV2Ray() to auto-install during setup()/connect(); +// running main() on import would trigger npm install + WireGuard MSI as a side effect. +const isDirectRun = process.argv[1] + && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (isDirectRun) { + main().catch(err => { + console.error(`[setup] Fatal: ${err.message}`); + process.exit(1); + }); +}