Skip to content

v0.9.45

Choose a tag to compare

@Goldentrii Goldentrii released this 01 Nov 12:04
· 41 commits to main since this release
22fe63f

Credential-safety and pricing-truth release. Proxy dial credentials are masked everywhere by default and can be revealed only on the local server with an explicit user confirmation; the hosted endpoint drops the obsolete monthly free-call cap and serves the read-only static-IP functions.

Added

  • novada_static_ip_mgmt gains a reveal_credentials action, local MCP server only (owner decision 2026-11-01, option A; final scope per a same-day follow-up decision below): static-ISP dial credentials stay masked everywhere by default, but a user who explicitly asks (e.g. to configure their own static IPs in AdsPower or another proxy-aware tool) can get the REAL account/password pair through this read-only action — gated behind the same two-step approval_token flow as open/renew (evaluateApprovalGate, ../utils/approval.ts), or a single MCP-elicitation-confirmed call on clients that support it (see the Security entry below). Filters are identical to list (status/region/key_word/page/limit/is_auto_renew); limit is additionally capped at 50 (the response says so when it clamps a higher requested value). The preview call (no approval_token) shows masked ip/port/region/expire_time rows only — no credentials — plus a count and a warning that the real credentials may be retained in the AI client's own chat history. The execute call (with the matching approval_token) returns {ip, port, region, expire_time, username, password} per row plus two ready-to-paste formats, dial_string (ip:port:username:password) and proxy_url (http://username:password@ip:port), built only from fields the API actually returned. Not available on the hosted endpoint — see below.

Changed

  • novada_static_ip_mgmt's plain read-only actions are now served on the hosted endpoint (mcp.novada.com): list/export are dispatchable there like any other tool (hosted tool count 28 → 29). Its two billable WRITE actions, open and renew, are still refused on hosted — and so, per a 2026-11-01 FOLLOW-UP, FINAL owner decision, is reveal_credentials: real dial credentials need a human-in-the-loop confirmation (MCP elicitation) that the stateless hosted endpoint cannot perform (see the Security entry below), and falling back to an agent-carried approval_token alone there would be the exact hole elicitation exists to close locally, so it is refused outright rather than served with a weaker guarantee. All three actions are refused at the ACTION level, inside the tool itself (assertActionAvailableOnRuntime(), thrown as a NovadaError before either the approval gate, elicitation, or any upstream call runs), not via whole-tool hiding. novada_static_ip_mgmt was removed from HOSTED_HIDDEN (hosted-server/vercel/api/mcp.ts) and its mirror (novada_discover's HOSTED_HIDDEN_TOOLS, npm-package/src/tools/discover.ts).

Changed (hosted server — mcp.novada.com, not part of the npm package)

  • The monthly free-call gateway cap is removed entirely (owner decision, 2026-10-29). mcp.novada.com's "1,000 free calls per token per month" limit was a one-time new-user promotion that has ended, not a recurring allowance — every call is now billed to the caller's own Novada account, same as calling the REST API directly; new accounts still get $10 free credits. Removed: the per-token monthly KV quota counter (decrementQuota/refundQuota/monthKey), the paid-tier plan resolution and over-cap exemption (enforceGatewayCap, resolvePlan, shouldAllowOverCap, classifyPlanFromUsageRecord, fetchAggregateBalance/parseBalanceValue, the approval-gate preview-charge exemption), and the FREE_PLAN_MONTHLY_QUOTA env var (no longer read). CAP_EXEMPT_TOOLS/ACCOUNT_TOOL_NAMES survive unchanged and now drive only the gateway status footer. Auth, rate limiting (60 req/min/IP), OAuth, and token caching are unchanged.
  • Gateway status footer collapses from three variants to two. The old "⚠ gateway: N/M free calls remaining this month · cost: unknown…" and "gateway: uncapped (paid account) · cost: unknown…" lines are gone. Billable tools now carry "gateway: usage is billed to your Novada account — balance and charges at dashboard.novada.com"; CAP_EXEMPT_TOOLS (novada_setup, novada_discover, novada_account + aliases) carry "gateway: no charge for this call". The over-cap rejection message ("## Free Gateway Cap Reached") and its _meta.quota_remaining success-path field are both gone.
  • Telemetry rows keep their schema (no DB migration) — charged/over_cap_allowed/quota_remaining/plan are now always null ("no cap applies") instead of a free/pro classification; validateToken no longer returns a meaningless quota_remaining. The HQ push payload shape is unchanged (buildHqPayload already defaulted these to 0/"" for a null row).
  • Updated the gateway's nightly contract canary (contract-test.py): COST_VISIBILITY now checks the two-variant footer contract; BILLING_TRUTH (which guarded a funded key against the now-removed cap) is deleted.
  • Docs/landing pages that claimed a free monthly call count (hosted-server/docs/*, hosted-server/landing/*, hosted-server/vercel/README.md, hosted-server/smithery.yaml) now describe usage-based billing with a $10 new-account credit instead.

Security

  • Static ISP IP dial credentials (account/password) are now masked in novada_static_ip_mgmt (list/export/open/renew) and in novada_plan_balance_all/novada_account(section="plans"/"summary")'s per-IP raw field — both hit the same /v1/static_house/list endpoint, which returns a per-IP dial username/password pair in cleartext. A new shared helper, maskCredentials() (_core/developer_api.ts), recursively masks a table-driven set of credential-bearing field names (password/passwd/pwd/pass/secret/token/api_key/apikey → ***; account/username/user_name → <PROXY_USER>, matching novada_proxy's existing convention) without touching IPs, ports, regions, statuses, or expiry dates. sanitizeServerMsg (_core/errors.ts) gained a matching rule so a credential echoed back in an UPSTREAM ERROR message is masked too, not just the success path. novada_capture_apikey's own existing ****<last4> masking is unchanged (out of scope — already correct for both its get and reset actions).
  • access_token added to the masked fields, plus a drift guard: a test reads the local developer-API/scraper-API reference mirror and fails if any credential-looking field name there is neither masked nor explicitly listed as safe (with a reason), so a future field such as dial_password cannot slip past the exact-match table unnoticed.
  • evaluateApprovalGate()'s signed payload now binds the caller's own tool name (../utils/approval.ts, 2026-11-01 follow-up to an independent security review of reveal_credentials): a new required toolName argument (e.g. "novada_static_ip_mgmt") is folded into the HMAC-signed canonical payload as { ...params, __tool: toolName } for all four gated tools (proxy_account_create, ip_whitelist add/del/remark, capture_apikey reset, static_ip_mgmt open/renew/reveal_credentials). Previously cross-tool replay was prevented only because each tool's gated params happened to differ in shape — a token minted for one tool could, in principle, verify unmodified against a different tool whose params object reduced to the same bytes (most exposed: proxy_account_create, which has no action discriminator field at all). A table-driven test now cross-checks every real gated call site pairwise.
  • novada_static_ip_mgmt's reveal_credentials action now uses real MCP elicitation as the human confirmation on the local stdio server, when the connected MCP client advertises elicitation support (clientCapabilities.elicitation.form, negotiated at initialize). Previously the only confirmation mechanism was a two-step approval_token round-trip that passes THROUGH the agent — a prompt-injected or careless agent could simply make both calls itself with no human ever seeing the request. With elicitation: a single tool call fetches the filtered IP count, sends the client a human-facing confirmation (count + a warning that credentials will appear in the conversation and may persist in the client's own chat history) via server.elicitInput, and only an explicit accept reveals the real username/password; decline/cancel/an elicitation-transport failure all return a non-throwing status:"declined" response with no credentials anywhere in it. approval_token is not read at all on this path — a pre-minted or replayed token has zero effect and cannot bypass the human-in-the-loop step. Local clients without elicitation support keep the unchanged approval_token fallback. The hosted endpoint, always — mcp.novada.com builds a fresh Server+StreamableHTTPServerTransport per stateless HTTP request and closes it again before any client reply could arrive, so a server-initiated request needing a later reply cannot complete there — cannot perform elicitation at all; per a same-day FOLLOW-UP, FINAL owner decision, falling back to the agent-carried approval_token alone on hosted was judged NOT an acceptable substitute (it would reopen the exact hole elicitation exists to close), so reveal_credentials is refused outright on hosted instead (see the Changed entry above) rather than left on that fallback. The capability check and elicitInput call live only in the stdio entry point (index.ts) and are injected into the tool as a plain callback (ElicitConfirmFn) — static_ip_mgmt.ts has no MCP-SDK/transport dependency and no forked logic between stdio and hosted.

Fixed

  • novada_static_ip_mgmt's list/export default to status="1" (in-use) when the caller omits status, instead of returning every IP the account has ever owned (including long-expired/released ones). A new summary field reports how many expired/released IPs exist and how to see them (status:"2"/status:"3"); an explicit status (including "" for all) is still honored exactly.
  • novada_plan_balance_all's static-ISP active_ip_count (surfaced via novada_account(section="plans"/"summary"), the HOSTED-reachable path) used to count IPs of EVERY lifecycle status as "active" (no status filter was sent). It now scopes to status="1" and adds expired_ip_count/released_ip_count as additive, backward-compatible fields (best-effort — a failed count probe omits the field rather than failing the whole response).
  • novada_account(section="traffic")'s card rendering no longer shows static ISP as a misleading 0.0 MB; it now renders the applicable:false "billed per-IP, not by traffic" note the JSON output always carried.
  • 3 novada_account backward-compat aliases (novada_wallet_balance, novada_capture_logs, novada_account_summary) silently dropped ALL caller args, including format — a caller asking for format:"json" always got a card back. They now forward ...args like their 3 siblings (wallet_usage_record/traffic_daily/plan_balance_all) already did.