diff --git a/README.md b/README.md index b3e23ec..b73c6f7 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ TypeScript, installed as executables on `PATH`. | [`openinstall`](#openinstall) | Give a repo an idempotent bin/install.sh: systemd, nginx + TLS, postgres, redis | | [`openmcp`](#openmcp) | The OpenMCP catalog of MCP relays: list, find a tool, call it, register your own | | [`shorten`](#shorten) | Mint a short link on the pit, and follow it from `/f/` | -| [`proxy`](#proxy) | Fetch through our paid residential proxies (Proxiware, Webshare); also a local forward proxy, an MCP server and a TUI | +| [`proxy`](#proxy) | Fetch through our paid residential proxies (Proxiware, Webshare, HProxy); also a local forward proxy, an MCP server and a TUI | | [`sysupdate`](#sysupdate) | Update this box: apt lists, apt packages, snaps | | [`scorecard`](#scorecard) | One weekly number for the whole fleet: traffic, channels, posts and ads, week over week | | [`users-dump`](#users-dump) | Every user account across the fleet, as one CSV | @@ -472,6 +472,8 @@ carries the same masked previews, not the values. | `proxiware_proxy_user`, `proxiware_proxy_password` | `PROXIWARE_PROXY_USER`, `PROXIWARE_PROXY_PASSWORD` | `proxy` | | `webshare` | `WEBSHARE_API_KEY` | `proxy` (status, and the proxy login when no user/password is set) | | `webshare_proxy_user`, `webshare_proxy_password` | `WEBSHARE_PROXY_USER`, `WEBSHARE_PROXY_PASSWORD` | `proxy` | +| `hproxy` | `HPROXY_API_KEY` | `proxy` (status, and generated lines when no user/password is set) | +| `hproxy_proxy_user`, `hproxy_proxy_password`, `hproxy_plan_id` | `HPROXY_PROXY_USER`, `HPROXY_PROXY_PASSWORD`, `HPROXY_PLAN_ID` | `proxy -p hproxy` | | `ngc`, `ngc_org` | `NGC_API_KEY`, `NGC_ORG` | `ngc` (when it has no key of its own), `nim` (last resort) | | `nvidia` | `NVIDIA_API_KEY` | `nim` | diff --git a/bin/cli-tools.ts b/bin/cli-tools.ts index 19bdd4a..4020d05 100755 --- a/bin/cli-tools.ts +++ b/bin/cli-tools.ts @@ -102,6 +102,7 @@ Keys (config set ): elevenlabs ELEVENLABS_API_KEY tts proxiware PROXIWARE_API_KEY proxy (status); proxiware_proxy_user/_password: traffic webshare WEBSHARE_API_KEY proxy (status, and the login when no user/password) + hproxy HPROXY_API_KEY proxy (status, and generated lines); hproxy_proxy_user/_password: traffic ngc NGC_API_KEY ngc, nim (fallback); ngc_org: NGC_ORG nvidia NVIDIA_API_KEY nim diff --git a/bin/proxy.ts b/bin/proxy.ts index 35c5927..ae19026 100755 --- a/bin/proxy.ts +++ b/bin/proxy.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * proxy — call the web through our paid proxies (Proxiware, Webshare). + * proxy — call the web through our paid proxies (Proxiware, Webshare, HProxy). * * proxy https://example.com body to stdout, US residential exit * proxy -i -c gb https://example.com @@ -38,7 +38,8 @@ if (isMain(import.meta.url)) { 'proxy: no proxy credentials. Pull them with `cli-tools config pull`, or set one:\n' + ' cli-tools config set proxiware_proxy_user\n' + ' cli-tools config set proxiware_proxy_password\n' + - ' cli-tools config set webshare # Webshare API key (login is read from it)\n', + ' cli-tools config set webshare # Webshare API key (login is read from it)\n' + + ' cli-tools config set hproxy # HProxy API key (lines are generated from it)\n', ); } diff --git a/package.json b/package.json index 3d3a916..16af237 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/cli-tools", - "version": "0.66.1", + "version": "0.67.0", "private": true, "description": "Local command-line tools, in TypeScript, exposed on PATH.", "type": "module", @@ -33,7 +33,7 @@ }, "dependencies": { "@profullstack/hqtui": "^0.6.0", - "@profullstack/proxy": "^0.1.0", + "@profullstack/proxy": "^0.2.0", "@resvg/resvg-js": "^2.6.2", "axe-core": "^4.13.0", "imapflow": "^1.7.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9169f9a..527e437 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^0.6.0 version: 0.6.0 '@profullstack/proxy': - specifier: ^0.1.0 - version: 0.1.0 + specifier: ^0.2.0 + version: 0.2.0 '@resvg/resvg-js': specifier: ^2.6.2 version: 2.6.2 @@ -393,8 +393,8 @@ packages: engines: {bun: '>=1.1', node: '>=22.6'} hasBin: true - '@profullstack/proxy@0.1.0': - resolution: {integrity: sha512-XobCE8NPNAzLcSEC8e8yXZI53/LCGLRTP3BLJKTIsQOHeyNxnH1oyKUSDB7SE1TwmWnCQnvjCkFLFMeKxshoaw==} + '@profullstack/proxy@0.2.0': + resolution: {integrity: sha512-ZRliOlSzaBsOjaMKCGG5CbH6mXR/2UPIDh27DpPUhtochKnRjD0jlfQhUjnFdjTvXEZVBFJCnyYfy9yT2uqpJQ==} engines: {node: '>=22.19'} hasBin: true @@ -1295,7 +1295,7 @@ snapshots: '@profullstack/hqtui@0.6.0': {} - '@profullstack/proxy@0.1.0': + '@profullstack/proxy@0.2.0': dependencies: undici: 8.11.2 diff --git a/src/credentials.ts b/src/credentials.ts index 20e7d62..9269bc6 100644 --- a/src/credentials.ts +++ b/src/credentials.ts @@ -54,6 +54,12 @@ export const KNOWN_KEYS: Record = { webshare: 'WEBSHARE_API_KEY', webshare_proxy_user: 'WEBSHARE_PROXY_USER', webshare_proxy_password: 'WEBSHARE_PROXY_PASSWORD', + // HProxy works the same way as Webshare: the API key alone is enough (the + // package generates a line from the account's plan), the login skips that. + hproxy: 'HPROXY_API_KEY', + hproxy_proxy_user: 'HPROXY_PROXY_USER', + hproxy_proxy_password: 'HPROXY_PROXY_PASSWORD', + hproxy_plan_id: 'HPROXY_PLAN_ID', // Read by `websearch` (Cloudflare Web Search). The token needs Workers AI // Read + AI Gateway Read; when it lacks them, email + global key are the // fallback. The account id is discovered from the credential when unset. diff --git a/src/proxy.ts b/src/proxy.ts index e09e4b8..25b6801 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -19,6 +19,10 @@ export const PROXY_VARIABLES = [ 'WEBSHARE_API_KEY', 'WEBSHARE_PROXY_USER', 'WEBSHARE_PROXY_PASSWORD', + 'HPROXY_API_KEY', + 'HPROXY_PROXY_USER', + 'HPROXY_PROXY_PASSWORD', + 'HPROXY_PLAN_ID', ] as const; // The store only keeps what KNOWN_KEYS names; a variable missing from there @@ -36,7 +40,9 @@ export function hasProxyLogin(env: Env): boolean { return Boolean( (env.PROXIWARE_PROXY_USER && env.PROXIWARE_PROXY_PASSWORD) || (env.WEBSHARE_PROXY_USER && env.WEBSHARE_PROXY_PASSWORD) || - env.WEBSHARE_API_KEY, + env.WEBSHARE_API_KEY || + (env.HPROXY_PROXY_USER && env.HPROXY_PROXY_PASSWORD) || + env.HPROXY_API_KEY, ); } diff --git a/src/registry.ts b/src/registry.ts index b2be6e0..9289410 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -68,7 +68,7 @@ const SUMMARIES: Record = { explee: 'Add more of your own leads to an Explee project: a CSV becomes a campaign with the same brief', fe: 'Forward Email aliases without the dashboard: list, ensure (idempotent), remove', dealsubs: 'Subscribe an inbox to coupon and deal newsletters, throttled, through TronBrowser', - proxy: 'Fetch through our paid proxies (Proxiware, Webshare); serve, MCP and TUI too', + proxy: 'Fetch through our paid proxies (Proxiware, Webshare, HProxy); serve, MCP and TUI too', scorecard: 'One weekly number for the whole fleet: traffic, channels, posts and ads, week over week', shorten: 'Mint a short link on the pit, and follow it from /f/', sysupdate: 'Update this box: apt lists, apt packages, snaps', diff --git a/test/proxy.test.ts b/test/proxy.test.ts index 0018967..e3221f1 100644 --- a/test/proxy.test.ts +++ b/test/proxy.test.ts @@ -4,6 +4,7 @@ import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { keyVariable, loadStored, saveStored } from '../src/credentials.ts'; +import { PROVIDERS } from '@profullstack/proxy'; import { hasProxyLogin, proxyEnv, PROXY_VARIABLES } from '../src/proxy.ts'; const dirs: string[] = []; @@ -37,6 +38,22 @@ describe('proxy keys', () => { expect(hasProxyLogin({ PROXIWARE_PROXY_USER: 'u', PROXIWARE_PROXY_PASSWORD: 'p' })).toBe(true); expect(hasProxyLogin({ WEBSHARE_API_KEY: 'k' })).toBe(true); expect(hasProxyLogin({ PROXIWARE_API_KEY: 'k' })).toBe(false); + expect(hasProxyLogin({ HPROXY_API_KEY: 'hpx_k' })).toBe(true); + expect(hasProxyLogin({ HPROXY_PROXY_USER: 'u' })).toBe(false); + expect(hasProxyLogin({ HPROXY_PROXY_USER: 'u', HPROXY_PROXY_PASSWORD: 'p' })).toBe(true); + }); + + it('take the HProxy keys from a vault pull, and nothing else', async () => { + const env = await sandbox(); + const pulled = { HPROXY_API_KEY: 'hpx_k', HPROXY_PLAN_ID: 'plan1', UNRELATED_SECRET: 'x' }; + const result = proxyEnv({ env, pull: () => pulled }); + expect(result.source).toBe('vault'); + expect(result.env.HPROXY_API_KEY).toBe('hpx_k'); + expect(result.env.HPROXY_PLAN_ID).toBe('plan1'); + expect(loadStored(env)).toEqual({ HPROXY_API_KEY: 'hpx_k', HPROXY_PLAN_ID: 'plan1' }); + expect(keyVariable('hproxy')).toBe('HPROXY_API_KEY'); + expect(keyVariable('hproxy_proxy_password')).toBe('HPROXY_PROXY_PASSWORD'); + expect(PROXY_VARIABLES).toContain('HPROXY_PROXY_USER'); }); }); @@ -97,8 +114,9 @@ describe('proxyEnv', () => { }); it('covers every variable the package reads', () => { - expect([...PROXY_VARIABLES].sort()).toEqual( - ['PROXIWARE_API_KEY', 'PROXIWARE_PROXY_PASSWORD', 'PROXIWARE_PROXY_USER', 'WEBSHARE_API_KEY', 'WEBSHARE_PROXY_PASSWORD', 'WEBSHARE_PROXY_USER'], - ); + // Read off the installed package, so a provider it adds fails here until + // its keys are pulled and stored too. HPROXY_PLAN_ID is read beside them. + const fromPackage = Object.values(PROVIDERS).flatMap((provider) => [provider.env.user, provider.env.password, provider.env.apiKey]); + expect([...PROXY_VARIABLES].sort()).toEqual([...fromPackage, 'HPROXY_PLAN_ID'].sort()); }); });