Commit Graph

1280 Commits

Author SHA1 Message Date
diegosouzapw
c4a993184e fix(quota): never flag rotating-refresh providers expired from the quota sync
The quota-sync path deliberately reuses a rotating-refresh provider's (Codex/
OpenAI/Claude — see refreshSerializer ROTATION_LOCK_GROUP) access_token WITHOUT
proactively refreshing it (#3019, to avoid the Auth0 family-revocation cascade).
When that token is expired the codex usage fetch returns "token expired", and
syncExpiredStatusIfNeeded then flagged the connection testStatus="expired" — a
false-negative: the credential is still valid (expires_at in the future) and the
reactive serialized 401 path refreshes the access_token on next use.

Symptom: freshly-added Codex accounts showed "expired" with no quota on the
quota page, while a providers-page refresh turned them green. They never lost
access — only the quota sync mislabeled them.

Fix: extract the decision into the pure, exported `quotaPathShouldMarkExpired()`
and skip rotating providers (rotationGroupFor !== null). Their status is owned by
the reactive path / connection test, never the quota sync. Adds unit coverage.
2026-06-02 08:47:51 -03:00
diegosouzapw
ccaa0b5f79 fix(quota): block qtSd models for keys with no quota-pool allocation (Check 2.9)
E2E testing on the VPS showed a normal key (empty allowedQuotas) could call a
qtSd/<group>/<provider>/<model> virtual model and route through a shared quota
pool — because the quota-exclusive enforcement (Check 3) only ran when
allowedQuotas was non-empty, so an unallocated key fell through to the normal
model checks and qtSd was served. This is the "empty allowedQuotas = all pools"
gap from the redesign.

Add Check 2.9 in enforceApiKeyPolicy: if the requested model is a qtSd model and
the key is NOT allocated to any quota pool (allowedQuotas empty), reject 403
QUOTA_NOT_ALLOCATED. Allocated keys are unchanged (Check 3 still validates scope).
This matches the owner's rule: only a key selected in a pool may use its qtSd
models. Normal (non-qtSd) model access for normal keys is unchanged.

Test: tests/unit/apikeypolicy-quota-only.test.ts — new case asserts a non-quota
key is blocked from qtSd (QUOTA_NOT_ALLOCATED) yet still uses normal models.
2026-06-02 07:40:08 -03:00
diegosouzapw
8277b98003 feat(api-manager): split keys into Normal vs Quota sections (compact 2-table layout)
The key list stacked many badges in one column (tall/cluttered) and didn't
distinguish quota keys. Now renders two sections — "Normal keys" and "Quota keys"
(purple QUOTA pill) — sharing the same compact table header via an extracted
renderKeyRow(). Quota rows prepend a qtSd-only mode chip + group-name chips
(resolved by fetching /api/quota/pools + /api/quota/groups → poolId→group map).
Empty sections are hidden. i18n en/pt-BR for the new labels.

Source-scan test + i18n parity in api-manager-quota-keys-section.test.ts.
2026-06-02 07:06:00 -03:00
diegosouzapw
8086d2878b fix(ws): codex Responses-over-WebSocket upgrade — clean handshake + bridge-secret auth
Two bugs made `wscat ws://host/v1/responses` fail with
"Transfer-Encoding can't be present with Content-Length":

1. authz/management policy 401'd the proxy's own internal authenticate/prepare
   loopback call to /api/internal/codex-responses-ws (MANAGEMENT-classified, the
   per-process bridge secret wasn't recognized one layer up). Added a tightly-scoped
   carve-out: isValidWsBridgeRequest() honors a timing-safe sha256 match of
   OMNIROUTE_WS_BRIDGE_SECRET (x-omniroute-ws-bridge-secret header) for that exact
   internal path; the route still re-validates the secret. → auth now succeeds → 101.

2. On auth failure the proxy spread the internal fetch's response headers onto the
   raw upgrade socket — a chunked Transfer-Encoding + Next CSP/route-class headers
   collided with writeHttpError's Content-Length framing (and duplicated Content-Type
   via a case-mismatched spread). writeHttpError now strips framing + pipeline/security
   headers (case-insensitive), and the auth-fail callsite no longer forwards them.

Regression test: tests/unit/responses-ws-proxy-headers.test.mjs (exports writeHttpError;
asserts no TE+CL, single Content-Type, no CSP/route-class leak, safe headers forwarded).
2026-06-02 06:02:49 -03:00
diegosouzapw
512e980a9e feat(quota): xiaomi monthly cap + deepseek USD preset; researched plan tiers
- xiaomi-mimo: token plan is MONTHLY (per platform.xiaomimimo.com/token-plan), so
  the seed is now tokens/monthly/4.1B (was weekly).
- deepseek: prepaid in USD — its balance API is already wired (deepseekQuotaFetcher)
  and the fair-share engine supports the usd unit (COUNTABLE_UNITS). Seeded a
  usd/monthly preset so the limit is set by dollar value.
- minimax: documented the real M3 tiers (Plus ~1.633B/Max ~5.053B/Ultra ~9.796B)
  in-comment; EPSILON keeps it manual until tier-aware presets land.
- planRegistry already seeds codex/claude/glm/minimax/kimi/kimi-coding/xiaomi-mimo/
  deepseek/bailian/alibaba; PoolWizard 'Limite' step stays editable.

Researched plan structures + the tier-aware-preset follow-up are in the redesign plan.
2026-06-02 05:17:20 -03:00
diegosouzapw
831f82858f feat(quota-share): beta banner, Responses + codex-WS endpoints in card, plan presets
- Beta banner scoped to the Quota Share page (functional-but-bugs-expected) with a
  pre-filled "open an issue" link (labels quota-share,beta). Page-only.
- Endpoints card now also surfaces POST /v1/responses (codex/github) and the
  codex-only WS /v1/responses line (the Responses-over-WebSocket proxy), each gated
  on the in-scope provider slug.
- planRegistry: seed xiaomi-mimo (4.1B-token weekly "lite" cap) and kimi-coding so the
  PoolWizard "Limite" step pre-fills a fair-share limit for these no-balance-API
  providers (fair-share enforces from the proxy's own token count, not an upstream
  balance — set the real cap manually in step 2).
- docs(API_REFERENCE): document the codex Responses-over-WebSocket endpoint.
- i18n en/pt-BR for all new keys.

Tracked in _tasks/features-v3.8.8/quota-share-key-redesign.plan.md (codex-WS config
toggle + per-provider balance fetchers + %-quota attribution are planned follow-ups).
2026-06-02 05:02:52 -03:00
diegosouzapw
11eb74c828 fix(quota-share): endpoints card default view shows real qtSd combos, not placeholders
The "Available endpoints" card's no-key (default) view generated representative
model ids from a hardcoded PREVIEW_MODELS_BY_PROVIDER map, so providers absent
from that map (claude, xiaomi-mimo, kimi-coding) rendered fake "model-a/b/c"
placeholders. It now fetches the REAL minted qtSd/* combos from /api/combos,
parses them (parseQuotaModelName), and groups by group → provider — falling back
to the placeholder map only when the fetch fails or returns nothing. The per-key
view already showed real models via /api/quota/keys/[id]/models; this aligns the
default view with it.

Verified on the local VPS: an exclusive key (share01) returns ONLY the real qtSd
models of its groups (claudao + chinas) and a non-quota key returns []. The
remaining /v1/models leak (non-quota keys still see qtSd among all models) is
tracked in the quota-key redesign plan.
2026-06-02 03:43:23 -03:00
Diego Rodrigues de Sa e Souza
fdbaae4734 fix(sse): stop infinite account-fallback loop on no-auth providers (#3061) (#3062)
No-auth / keyless providers (opencode, opencode-zen) returned synthetic
"noauth" credentials BEFORE honoring excludeConnectionIds, so the chat
account-fallback loop re-selected the same synthetic connection forever on
a persistent upstream error (e.g. the opencode public endpoint answering
401 "Model X is not supported"). The synthetic id has no DB row, so
markAccountUnavailable could not persist a cooldown to brake it — each
iteration wrote key-health + request logs immediately, growing the DB until
the disk filled (see @paraflu's "failure #320" trace in discussion #3038).

Honor the exclusion set in both synthetic-credential paths
(getProviderCredentials NOAUTH_PROVIDERS block + opencode-zen keyless
fallback): once "noauth" is already excluded, return null so the handler
stops after a single attempt. The happy path (nothing excluded -> synthetic
noauth) is preserved, so keyless access still works.

Closes #3061.

Tests (TDD): tests/unit/auth-noauth-fallback-loop-3061.test.ts — the two
exclusion cases failed before the fix and pass after; two happy-path guards
ensure first-selection synthetic noauth still resolves.
2026-06-02 03:29:10 -03:00
dangeReis
dee20ac665 fix(pii): preserve custom event names and apply stream PII sanitization fixes (#3059)
* fix(sse): defer enqueuing of event lines to align event names with data lines and prevent stop-signal event name misattribution

* fix(sse): preserve keep-alives and prevent pending event leakage on dropped chunks

* fix(sse): preserve pending event lines before other non-data lines and fix zero-window-size bypass

* fix(sse): defer lastEventLine update until after flush check to preserve previous event context on flush

* fix(sse): flush trailing pendingEventLine when stream closes

* fix(sse): preserve consecutive event lines without intervening data

---------

Co-authored-by: Ruslan Sivak <russ@ruslansivak.com>
2026-06-02 02:17:52 -03:00
diegosouzapw
66ddbb0f5a chore: remove Petals executor and tighten route typing
Remove the Petals executor from registration and exports.

Improve type safety by replacing broad any usage in MCP tool registration
with inferred types and documenting dynamic handler type limitations.

Add request validation for the agent bridge cert route and expand tests to
ensure switch buttons explicitly declare type="button", preventing implicit
form submissions.
2026-06-02 02:10:34 -03:00
diegosouzapw
3dca2bb3f1 fix(quota-share): hidden pools, delete-group UI, endpoints card (Anthropic + collapse)
Bugs found while testing the Quota Share engine on the local VPS:

- B1 hidden/stuck pools: pools created while the page group filter was "all"
  were persisted with group_id="all", matched no real group, and rendered
  nowhere — so they could not be seen, edited or deleted. PoolWizard now resolves
  the group id away from the "all" sentinel before POST/PATCH (falls back to the
  first real group / seed group-demo), and QuotaSharePageClient renders an
  "Ungrouped" recovery bucket so already-orphaned pools stay editable/deletable.
- B3 one-connection-per-pool made explicit: existingPoolConnectionIds now spans
  every member connection (not just the primary), and the wizard shows which pool
  an already-used connection belongs to instead of silently disabling it.
- B4 delete group: wired the missing UI control + handler (handleDeleteGroup,
  409-aware) — the backend DELETE handler + deleteGroup already existed. Hidden for
  "all" and the protected seed group-demo.
- B5a endpoints card now surfaces the native Anthropic POST /v1/messages line when
  a claude*/anthropic provider is in scope (previously only /v1/chat/completions).
- B5b endpoints card gained a collapse/minimize toggle (the card was too tall).

Source-scan tests + en/pt-BR i18n parity in quota-share-bugfixes-v388.test.ts.
The larger quota-key redesign (key type bound to a group, default-restricted with
opt-in normal-model access, recoverable keys, api-keys page layout) is planned
separately in _tasks/features-v3.8.8/quota-share-key-redesign.plan.md.
2026-06-02 02:07:24 -03:00
diegosouzapw
8386ab3084 fix: complete #3054 dead-provider removal (petals executor + stale tests)
#3054 ("remove 9 dead/unreachable free providers") removed the petals/nanobanana
configs, registry entries and validators but left dangling references that broke
the build and the unit suite on release/v3.8.8:

- open-sse/executors/petals.ts imported the deleted ../config/petals.ts
  (webpack "Module not found" → `next build` failed). Removed the executor, its
  registration + re-export in executors/index.ts, and the leftover
  `providerId === "petals"` branch in providerAllowsOptionalApiKey.
- Removed tests for the now-deleted providers: executor-petals.test.ts and
  poolside-provider.test.ts (REGISTRY.poolside was removed), and the petals /
  nanobanana validator assertions in provider-validation-specialty.test.ts,
  plus the stale petals catalog assertions in providers-page-utils.test.ts,
  proxy-connection-test.test.ts and providers-route-managed-catalog.test.ts.

The image/video/embed registries for nanobanana/replicate/nomic are real and
untouched — only the dead chat/api-key surfaces were removed. 146/146 affected
tests pass; typecheck / build clean.
2026-06-02 00:23:51 -03:00
dangeReis
c711ac62a7 fix(pii): preserve custom event names and apply stream PII sanitization fixes (#3056)
Co-authored-by: Ruslan Sivak <russ@ruslansivak.com>
2026-06-01 23:13:19 -03:00
Diego Rodrigues de Sa e Souza
a5f3c998c1 fix(sse): auto-calibrate heap-pressure threshold to the V8 heap ceiling (#3052)
Threshold now derives from the live V8 heap ceiling (85%, floor 400MB) instead of a fixed 200MB that sat below the ~260MB app baseline and 503'd every request. Tracks --max-old-space-size across 1GB/2GB/large VPS.
2026-06-01 21:12:37 -03:00
terence71-glitch
08c70572fb fix(proxy): resolve registry assignments for combo and key levels (#3048)
* fix(proxy): resolve registry assignments for combo and key levels

* fix(proxy): guard registry scope level lookup
2026-06-01 19:47:46 -03:00
terence71-glitch
5350b5e7f5 fix(proxy): fail closed for OAuth usage account proxies (#3051) 2026-06-01 19:47:31 -03:00
diegosouzapw
8f0615fd04 fix(security): resolve 5 CodeQL alerts, document FPs, harden deploy skills
Real fixes (alerts auto-close on next scan):
- agentSkills/generator: escape backslash before double-quote in YAML
  frontmatter so a trailing backslash can't escape the closing quote
  (js/incomplete-sanitization #297/#298)
- usage: replace /\s*\(RESTRICTED\)\s*$/ with a non-backtracking literal +
  trim (js/polynomial-redos #275)
- i18n/request: skip __proto__/constructor/prototype in deepMergeFallback
  (js/prototype-pollution-utility #274)
- scripts/ad-hoc/nvidia diag: log key presence only, never key chars
  (js/clear-text-logging #273)
- tests: regression coverage for all three production fixes (YAML round-trip,
  proto-pollution guard, RESTRICTED strip + ReDoS timing)

Docs:
- ARCHITECTURE.md: executor count 45->55, OAuth modules 15->16, agy.ts in list

Deploy skills (deploy-vps-*-cc/-cx/-ag): add --legacy-peer-deps (npm v11
peer-dep resolver crashes on the omniroute tree) and replace the
"; pm2 start" that masked a failed install with a proper && chain.
2026-06-01 19:36:48 -03:00
Paijo
9e7f3cad10 feat(plugins): plugin hook wiring + comprehensive test suite + welcome-banner example (#3045)
Follow-up to the plugins framework (#3041): wires the plugin hooks end-to-end and adds full test coverage.

- Wires `onRequest` / `onResponse` / `onError` hooks into the chat pipeline (`chatCore` now imports from the unified `hooks` registry).
- Loads active plugins on server startup (`pluginManager.loadAll()` in `server-init`) so they survive restarts.
- Ships a `welcome-banner` example plugin (`examples/plugins/`) + test fixture.
- Adds a comprehensive plugin test suite (manifest, db, config, hooks, manager lifecycle, loader IPC, permissions, scanner, welcome-banner e2e).

Integration fixes applied during review:
- `manager.install` now removes an orphaned `destDir` (DB row gone but files left on disk) before the atomic rename, guarded by path containment — it previously failed with `ENOTEMPTY` (a regression surfaced by the new lifecycle test).
- `plugins-db` / `plugins-manager-lifecycle` tests now initialize the DB via the real migration `076` (`getDbInstance`) rather than relying on ambient state, so a missing/renumbered migration fails loudly instead of being masked.

348/348 plugin tests pass; typecheck / cycles clean.

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>
2026-06-01 16:20:11 -03:00
Diego Rodrigues de Sa e Souza
20c31493af feat(plugins): plugins framework + per-API-key disable-non-public-models (#3041)
Integrates two community contributions into release/v3.8.8 with security hardening and conflict resolution.

- **Plugins framework** (#2913 — thanks @oyi77): hooks + registry unification, plugin SDK (`definePlugin`), worker-thread sandbox, per-plugin hook rate limiting, SHA-256 integrity verification, semver-gated upgrade, and execution analytics. Plugin routes are loopback-only (`isLocalOnlyPath`); `child_process` exec is opt-in via `OMNIROUTE_PLUGINS_ALLOW_EXEC` (default off).
- **API key option: disable non-published models** (#3017 — thanks @androw): a per-key flag restricting the key to discovered public models (combos / `auto/*` / `qtSd/*` routing still allowed).

Hardening applied during integration: migration renumber (089/090/091), `/api/plugins` LOCAL_ONLY route-guard classification (closes the plugin-RCE vector), atomic install/upgrade with path containment, `O_EXCL` tmp-file creation (TOCTOU), rate-limit-map eviction, `validatePluginConfig` on configure, `buildErrorBody` on all plugin error paths. 246/246 tests; typecheck / cycles / docs-sync clean.

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
2026-06-01 15:43:55 -03:00
Paijo
89c52d4f04 fix: resolve pre-existing test failures (env sync, PII, quota, sidebar) (#3039)
Integrated into release/v3.8.8. 9 legit test alignments + 34 new test files kept; restored 5 masked assertions (sk_ key redaction, circular/SSN redaction, T24 wait-log, THEORY-004 SSE, relay handoff) to strict/meaningful checks. Thanks @oyi77!
2026-06-01 14:52:50 -03:00
Hernan Javier Ardila Sanchez
482e4690eb fix(dashboard): use lightweight ping endpoint for MaintenanceBanner (fixes #3040) (#3043)
Integrated into release/v3.8.8. Applied review fixes: moved the SELECT 1 into a pingDb() db helper (no raw SQL in route, Hard Rule #5) + the 503 catch no longer leaks err.message (Hard Rule #12). Thanks @herjarsa!
2026-06-01 14:31:42 -03:00
Chewji
6b7ae9ad2e fix(mcp): resolve streamable http transport readiness offline status (#3037)
Integrated into release/v3.8.8. Fixes MCP streamable-HTTP transport readiness reporting when offline + session sweep. Thanks @Chewji9875!
2026-06-01 09:59:23 -03:00
dangeReis
e11366b647 fix(privacy): resolve PII feature flag correctly and fix PII response sanitization in streaming SSE requests (#3021)
Integrated into release/v3.8.8. Fixes the PII feature-flag resolution + adds streaming-aware SSE PII sanitization (rolling-window transform, obfuscation-hardened regexes, Luhn/CPF/CNPJ checks). Applied review fixes: block-mode + fallback errors no longer leak pattern types / upstream err.message into stream errors (Hard Rule #12); typed the createPiiTransform cast (no any); fixed a string===boolean type error; dropped the generated .source/* files. Thanks @dangeReis!
2026-06-01 08:22:09 -03:00
Insomnia
baa4e56997 fix: improve macOS Electron window chrome (#3029)
Integrated into release/v3.8.8. macOS Electron window-chrome: tray icon template + draggable header region (excludes interactive controls) with a safe fallback + MutationObserver cleanup. Squash-merged (drops the AI co-author trailers per repo policy). Thanks @bobbyunknown!
2026-06-01 08:08:01 -03:00
mi
57dfa25312 fix(oom): prevent per-request memory accumulation (256MB heap) (#2973)
Integrated into release/v3.8.8. OOM fix: truncateForLog caps logged bodies at 8KB (prevents multi-MB clone accumulation across log call-sites) + a heap-pressure 503 guard. Applied review fix: the 503 body no longer leaks the heap figure (Hard Rule #12) — logged internally instead. Thanks @soyelmismo!
2026-06-01 08:07:03 -03:00
CitrusIce
c34285f676 fix(stream): drop leaked chat bootstrap chunk for responses clients (#3035)
Integrated into release/v3.8.8. Drops the leaked empty chat.completion.chunk bootstrap frame before response.* SSE events so strict /v1/responses clients (OpenCode) don't fail on the first frame. Thanks @CitrusIce!
2026-06-01 07:56:25 -03:00
guanbear
dd42b1564f Fix missing API key scope translations (#3031)
Integrated into release/v3.8.8. Fills the 7 apiManager.* self-service scope keys (managementAccessDesc, selfServiceVisibility(+Desc), ownUsageVisibility(+Desc), sharedAccountQuotaVisibility(+Desc)) across 41 locales + regression test. Resolved conflicts against the current release on your branch. Thanks @guanbear!
2026-06-01 07:50:58 -03:00
Paijo
e4eeb6dc7f refactor: Make SessionPool modular & provider-agnostic (#2978)
Integrated into release/v3.8.8. Applied review fixes: typed DefaultExecutor.execute(input: ExecuteInput), guarded result?.response?.status, removed the unused cloakbrowser dependency. Session-pool modular refactor + 36 tests green. Thanks @oyi77!
2026-06-01 07:44:52 -03:00
Paijo
4de674e0fc test: increase coverage to >60% — 353 new test cases (#3018)
Integrated into release/v3.8.8 — 122 new passing tests (DB core/models/settings, executor base, stream payload collector, model resolver). Thanks @oyi77!
2026-06-01 07:39:09 -03:00
Diego Rodrigues de Sa e Souza
099cb67317 Merge pull request #3032 from diegosouzapw/feat/quota-share-v2
feat(quota): Quota Share v2 — nav move, 3-col grouped layout, endpoints+key preview, full pool edit
2026-06-01 07:03:14 -03:00
diegosouzapw
df7b1e83c2 feat(quota): available-endpoints card + per-key model preview 2026-06-01 03:22:54 -03:00
diegosouzapw
6848636351 feat(quota): edit button opens full PoolWizard (exclusive-preserving); retire EditAllocationsModal
- PoolWizard: add editPoolExclusive prop; pre-fill uses editPoolExclusive ?? false so editing an exclusive pool keeps it exclusive instead of silently clearing the flag
- QuotaSharePageClient: wire editing state to a second <PoolWizard editPool=... editPoolExclusive=...>; compute editingExclusive from apiKeys.allowedQuotas; remove EditAllocationsModal import and handleSaveAllocations
- Delete EditAllocationsModal.tsx (fully subsumed by PoolWizard step 3)
- Tests: quota-email-privacy + quota-groups-ui + quota-pool-wizard updated to reflect retirement; new quota-edit-opens-wizard.test.ts (9 source-scan assertions)
2026-06-01 03:15:21 -03:00
diegosouzapw
3c8e84d702 feat(quota): all-groups default + stacked group sections + 3-col cards
- selectedGroupId defaults to "all" instead of "group-demo"
- <select> prepends <option value="all">{t("allGroups")}</option>
- Pool list replaced by groupsToRender map: one stacked section per group
  (heading: group name + count via groupPools.length), each with a
  grid-cols-1 md:grid-cols-2 xl:grid-cols-3 card grid
- Rename button guard changed from selectedGroupId !== "group-demo" to
  selectedGroupId !== "all" (no single target when viewing all groups)
- i18n: allGroups added to en.json ("All groups") + pt-BR.json ("Todos os grupos")
- quota-share-layout-v2.test.ts: 10 source-scan + i18n parity assertions
- quota-groups-ui.test.ts: align group heading test to groupPools.length
2026-06-01 03:03:03 -03:00
diegosouzapw
ac5ac0c2a7 fix(api): guard rotating providers in manual token-refresh route (Codex family-revocation)
POST /api/providers/[id]/refresh was the last unguarded proactive-refresh
entry point for rotating-refresh providers. The dashboard auto-refreshes every
expiring connection on a page load (and an old cached frontend bulk-calls this
endpoint), so each Codex account's single-use refresh_token got rotated; Auth0
then revoked the whole token family (openai/codex#9648) — every account but the
last died with [403] <!DOCTYPE. refreshAndUpdateCredentials (quota-sync) and the
connection-test route were already guarded; this closes the gap.

The route now skips proactive rotation when rotationGroupFor(provider) !== null
and returns {success,skipped,message}, deferring genuine expiry to the reactive,
serialized 401 path. Non-rotating providers keep refreshing on demand.

Test: codex-manual-refresh-rotating-guard (source-assertion, matches the
token-refresh-race-comprehensive #2941 style).
2026-06-01 03:00:13 -03:00
diegosouzapw
b64489b3ad feat(quota): PoolWizard editPool mode (full pool edit via PATCH)
- Add `editPool?: QuotaPool` to PoolWizardProps; import QuotaPool type
- Open effect pre-fills state (connectionIds, name, groupId, allocations,
  plan dims) from editPool; create-reset unchanged when editPool absent
- handleFinish branches on editPool: create path keeps POST→PUT→PATCH
  unchanged; edit path sends a single PATCH (name+groupId+connectionIds+
  allocations+exclusive) then optional PUT when dimensionsEdited
- Title uses t("editPoolTitle"), submit button uses t("saveChanges") in edit mode
- Add editPoolTitle/saveChanges to en.json and pt-BR.json (quotaShare namespace)
- New source-scan test: quota-pool-wizard-edit.test.ts (15 assertions, all pass)
2026-06-01 02:52:51 -03:00
diegosouzapw
881a8e9b56 feat(quota): move Quota Share nav item under Provider Quota 2026-06-01 02:43:59 -03:00
diegosouzapw
342f12b77a feat(quota): GET /api/quota/keys/[id]/models — preview the qtSd/ models a key sees 2026-06-01 02:39:37 -03:00
diegosouzapw
e5392a7eaa fix(quota): pool PATCH prunes OLD group/provider combos before re-sync (no orphan qtSd/ on switch) 2026-06-01 02:35:26 -03:00
diegosouzapw
e694674851 feat(quota): pool PATCH accepts groupId + connectionIds, re-syncs combos on connection change 2026-06-01 02:28:21 -03:00
Diego Rodrigues de Sa e Souza
4b3987e190 Merge pull request #3022 from diegosouzapw/feat/quota-share-redesign
feat(quota): Pool Groups + correções de enforcement → v3.8.8 (+ authz peer-IP, screen fixes)
2026-05-31 22:05:18 -03:00
diegosouzapw
58a7d97b3f Merge branch 'release/v3.8.8' into feat/quota-share-redesign
# Conflicts:
#	src/lib/db/apiKeys.ts
#	src/lib/db/migrationRunner.ts
#	src/server/authz/policies/management.ts
#	src/server/authz/routeGuard.ts
#	tests/unit/route-guard-private-lan.test.ts
2026-05-31 21:08:30 -03:00
diegosouzapw
137d77cf12 feat(quota): group selector + grouped pool cards + wizard group pick (Interface A)
Task B9: QuotaSharePageClient fetches /api/quota/groups, renders a group bar
(select + New group + Rename group actions), and filters pool cards by the
selected group. PoolWizard adds a group picker in step 1 and POSTs groupId;
default pool name now uses provider slug instead of the raw connection
label/email. EditAllocationsModal adds a group allocation note. i18n parity
for 7 new quotaShare keys in en + pt-BR. PoolCreateSchema gains optional
groupId field. Test: tests/unit/quota-groups-ui.test.ts (21 source-scan
assertions, all green).
2026-05-31 19:52:00 -03:00
diegosouzapw
5d84a8fa7a test(quota): combo protection guard matches qtSd/ prefix 2026-05-31 19:33:24 -03:00
diegosouzapw
efa8f0af23 feat(quota): /api/quota/groups CRUD (rename re-syncs combos)
Add GET/POST /api/quota/groups and PATCH/DELETE /api/quota/groups/[id].
PATCH rename calls renameGroup then re-syncs quotaShared-* combos (via
dynamic-import syncQuotaCombos) for every pool in the group, since combo
names embed the group slug. DELETE maps the deleteGroup throw (protected
group-demo or pools still referencing the group) to 409 Conflict via
buildErrorBody — never 500. All handlers are management-gated via
requireManagementAuth and route all errors through buildErrorBody (Hard
Rule #12). 37-assertion source-scan test verifies auth, error sanitisation,
response shapes, Zod validation, PATCH re-sync wiring, and DELETE 409 logic.
2026-05-31 19:31:30 -03:00
diegosouzapw
eac24ca458 feat(quota): group-level allocations + group access enforcement
upsertAllocations now propagates allocation rows to every pool in the
same group. A key allocated to pool A in group G automatically gets
an allocation row in pool B (same group), so enforceQuotaShare finds
the row when the key calls pool B's model and applies fair-share.

No change to apiKeyPolicy Check 3 (already group-correct via B5
groupSlug logic) or enforce.ts pool-match (works once propagation
supplies the row).
2026-05-31 19:24:59 -03:00
diegosouzapw
e9cc53b17f feat(quota): key scope + /v1/models expand to the whole group (real group)
- `getPoolsByGroup(groupId)` in quotaPools.ts: SELECT all pools for a group,
  exposed as public API and re-exported from localDb.ts.
- `resolveQuotaKeyScope`: pool → group → all pools in group expansion.
  The returned `poolSlugs` field now holds GROUP slugs (quotaGroupSlug of the
  group name) instead of individual pool-name slugs, one per distinct group.
  Group slug included only when the group has ≥1 valid connection (group-level
  anyValidConnection gate). Deduplication: a key in 2 pools of the same group
  expands once.
- `filterModelsToQuotaPools` (quotaCombos.ts): already matches by
  `parsed.groupSlug` ∈ poolSlugs — no logic change needed, just test alignment.
- quota-key-resolve.test.ts: updated 4 tests that asserted pool-name slugs
  (testpoola2, poolmix, etc.) to assert the group slug ("groupdemo") — this is
  alignment to the B5 group semantics, not masking; the assertions were correct
  for the old pool-slug behavior and are now correct for the new group-slug
  behavior.
- quota-catalog-filter.test.ts: updated fixtures from old quotaShared-* format
  to qtSd/<groupSlug>/... (naming changed in B3, test was already broken before B5).
2026-05-31 19:15:16 -03:00
diegosouzapw
6d2e695882 fix(sse): stop Codex multi-account family-revocation cascade on quota-sync
The Quota / Providers dashboard (POST /api/usage/provider-limits ->
syncAllProviderLimits, GET /api/usage/[connectionId]) calls
refreshAndUpdateCredentials() per connection, in concurrent chunks. For
rotating-refresh providers (Codex/OpenAI share one Auth0 client_id) the
single-use refresh_token is rotated on every refresh; refreshing siblings
concurrently makes Auth0 revoke the whole token family (openai/codex#9648),
killing every account but the last with [403] <!DOCTYPE html>. On the affected
VM expires_at was persisted as ~0 so needsRefresh() was effectively always
true -> every codex account refreshed on every page visit -> guaranteed cascade.

Fix #1: refreshAndUpdateCredentials skips proactive refresh for rotating
providers (rotationGroupFor) and reuses the current access_token for the quota
fetch; genuine expiry is handled by the reactive, serialized 401 path.

Fix #2 (defense in depth): serializeRefresh inserts a settle gap between two
QUEUED sibling refreshes (default 2000ms, CODEX_REFRESH_SPACING_MS, '0' to opt
out) but releases a lone refresh immediately, adding no latency to the reactive
request path.

Tests: codex-quota-sync-no-proactive-refresh (skip + non-rotating guard),
refresh-serializer-spacing (default/opt-out + lone-vs-queued behavior).
2026-05-31 19:13:41 -03:00
diegosouzapw
fc6692925e feat(quota): group-aware quotaShared combos (qtSd/<group>/...) with provider-scoped prune
- resolvePoolForSync now resolves groupName via getGroupName(pool.groupId),
  falling back to pool.name when the group record is missing.
- Both quotaModelName calls in syncQuotaCombos use groupName instead of
  pool.name, so combos are named qtSd/<groupSlug>/<provider>/<model>.
- Prune is now scoped to group+provider: syncing pool A (openrouter) never
  deletes pool B (baidu) combos that share the same group.
- removeQuotaCombosForPool likewise scoped to group+provider.
- Updated quota-combos-sync, quota-combo-balancing, quota-combo-cli-providers
  tests for the new group-slug naming.
- New tests/unit/quota-combo-groups.test.ts: G1–G4 cover two-pool same-group
  naming, provider-scoped prune isolation, default-group fallback, and stale
  prune within the same group+provider.
2026-05-31 19:02:32 -03:00
diegosouzapw
cb1a18fa1a feat(quota): qtSd/<group>/<provider>/<model> model naming
Replace the old quotaShared-<poolSlug>-<provider>/<model> format with
qtSd/<groupSlug>/<provider>/<model>. Adds quotaGroupSlug() as the canonical
helper; keeps quotaPoolSlug() as a delegating alias so existing callers
(quotaKey.ts, quotaCombos.ts) compile without changes. parseQuotaModelName
now returns { groupSlug, provider, model }. Updates quotaCombos.ts,
apiKeyPolicy.ts, and all tests that referenced the old field name or the
old hardcoded prefix literal.
2026-05-31 18:48:41 -03:00
diegosouzapw
1eff658678 feat(quota): quotaGroups DB module (CRUD)
Add src/lib/db/quotaGroups.ts with createGroup/getGroup/getGroupName/
listGroups/renameGroup/deleteGroup; deleteGroup guards against non-empty
groups (throws /pools/i) and protects the 'group-demo' seed. Re-export
all functions + QuotaGroup type from localDb.ts. 15 unit tests in
tests/unit/quota-groups-crud.test.ts all passing.
2026-05-31 18:32:50 -03:00