Repository navigation
v0.9.45
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_mgmtgains areveal_credentialsaction, 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 REALaccount/passwordpair through this read-only action — gated behind the same two-stepapproval_tokenflow asopen/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 tolist(status/region/key_word/page/limit/is_auto_renew);limitis additionally capped at 50 (the response says so when it clamps a higher requested value). The preview call (noapproval_token) shows maskedip/port/region/expire_timerows 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 matchingapproval_token) returns{ip, port, region, expire_time, username, password}per row plus two ready-to-paste formats,dial_string(ip:port:username:password) andproxy_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/exportare dispatchable there like any other tool (hosted tool count 28 → 29). Its two billable WRITE actions,openandrenew, are still refused on hosted — and so, per a 2026-11-01 FOLLOW-UP, FINAL owner decision, isreveal_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-carriedapproval_tokenalone 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 aNovadaErrorbefore either the approval gate, elicitation, or any upstream call runs), not via whole-tool hiding.novada_static_ip_mgmtwas removed fromHOSTED_HIDDEN(hosted-server/vercel/api/mcp.ts) and its mirror (novada_discover'sHOSTED_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 theFREE_PLAN_MONTHLY_QUOTAenv var (no longer read).CAP_EXEMPT_TOOLS/ACCOUNT_TOOL_NAMESsurvive 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_remainingsuccess-path field are both gone. - Telemetry rows keep their schema (no DB migration) —
charged/over_cap_allowed/quota_remaining/planare now alwaysnull("no cap applies") instead of a free/pro classification;validateTokenno longer returns a meaninglessquota_remaining. The HQ push payload shape is unchanged (buildHqPayloadalready defaulted these to0/""for anullrow). - Updated the gateway's nightly contract canary (
contract-test.py):COST_VISIBILITYnow 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 innovada_plan_balance_all/novada_account(section="plans"/"summary")'s per-IPrawfield — both hit the same/v1/static_house/listendpoint, 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>, matchingnovada_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 itsgetandresetactions). access_tokenadded 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 asdial_passwordcannot 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 ofreveal_credentials): a new requiredtoolNameargument (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_whitelistadd/del/remark,capture_apikeyreset,static_ip_mgmtopen/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 noactiondiscriminator field at all). A table-driven test now cross-checks every real gated call site pairwise.novada_static_ip_mgmt'sreveal_credentialsaction 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 atinitialize). Previously the only confirmation mechanism was a two-stepapproval_tokenround-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) viaserver.elicitInput, and only an explicitacceptreveals the realusername/password;decline/cancel/an elicitation-transport failure all return a non-throwingstatus:"declined"response with no credentials anywhere in it.approval_tokenis 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 unchangedapproval_tokenfallback. The hosted endpoint, always —mcp.novada.combuilds a freshServer+StreamableHTTPServerTransportper 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-carriedapproval_tokenalone on hosted was judged NOT an acceptable substitute (it would reopen the exact hole elicitation exists to close), soreveal_credentialsis refused outright on hosted instead (see the Changed entry above) rather than left on that fallback. The capability check andelicitInputcall live only in the stdio entry point (index.ts) and are injected into the tool as a plain callback (ElicitConfirmFn) —static_ip_mgmt.tshas no MCP-SDK/transport dependency and no forked logic between stdio and hosted.
Fixed
novada_static_ip_mgmt'slist/exportdefault tostatus="1"(in-use) when the caller omitsstatus, instead of returning every IP the account has ever owned (including long-expired/released ones). A newsummaryfield reports how many expired/released IPs exist and how to see them (status:"2"/status:"3"); an explicitstatus(including""for all) is still honored exactly.novada_plan_balance_all's static-ISPactive_ip_count(surfaced vianovada_account(section="plans"/"summary"), the HOSTED-reachable path) used to count IPs of EVERY lifecycle status as "active" (nostatusfilter was sent). It now scopes tostatus="1"and addsexpired_ip_count/released_ip_countas 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 misleading0.0 MB; it now renders theapplicable:false"billed per-IP, not by traffic" note the JSON output always carried.- 3
novada_accountbackward-compat aliases (novada_wallet_balance,novada_capture_logs,novada_account_summary) silently dropped ALL caller args, includingformat— a caller asking forformat:"json"always got a card back. They now forward...argslike their 3 siblings (wallet_usage_record/traffic_daily/plan_balance_all) already did.