Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions ai-path/FAILURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 16 additions & 3 deletions ai-path/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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[]`

Expand Down Expand Up @@ -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. |
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions ai-path/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 ──────────────────────────────────────────────────────
Expand Down
55 changes: 51 additions & 4 deletions ai-path/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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() ────────────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
".": {
Expand All @@ -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"
Expand Down Expand Up @@ -49,6 +51,7 @@
"errors/",
"examples/",
"docs/",
"ai-path/",
"bin/setup.js",
"dist/",
"src/",
Expand Down
34 changes: 25 additions & 9 deletions setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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:');
Expand All @@ -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);
});
}
Loading