mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* docs(security): add BAN_DETECTION.md — banned-keyword / account-ban detection (#5600) New docs/security/BAN_DETECTION.md documenting the previously-undocumented system: - the 8 built-in ACCOUNT_DEACTIVATED_SIGNALS + custom keywords are additive - detection flow (body substring match -> terminal `banned` state, skipped in account selection; `deactivated` on 401/403; autoDisableBannedAccounts) - scope: global (all providers); the signal strings target OAuth/subscription scrapers - custom keywords: add path, 200-char cap, hot-reload, and the false-positive warning (raw substring match -> prefer full ban sentences, not "quota"/"limit") - recovery: terminal states never auto-recover -> re-test / re-auth / re-enable Registered in security meta.json; cross-linked from RESILIENCE_GUIDE (terminal states). Docs-only. Closes #5600 * docs(security): clarify deactivated vs expired terminal-status split (#5600) The same ACCOUNT_DEACTIVATED signal surfaces as two different terminal statuses depending on the code path: chatCore.ts inline writes 'deactivated' (401/403 via classifyProviderError), while markAccountUnavailable() -> resolveTerminalConnectionStatus() writes 'expired'. Document both.
This commit is contained in:
committed by
GitHub
parent
aa0ac7da87
commit
57794cb8d8
@@ -81,7 +81,7 @@ OmniRoute has three distinct but related resilience mechanisms. Each has a diffe
|
||||
|
||||
**Terminal states (NOT cooldowns):**
|
||||
|
||||
- `banned`
|
||||
- `banned` — set by banned-keyword / account-ban detection (see [BAN_DETECTION](../security/BAN_DETECTION.md))
|
||||
- `expired`
|
||||
- `credits_exhausted`
|
||||
|
||||
|
||||
137
docs/security/BAN_DETECTION.md
Normal file
137
docs/security/BAN_DETECTION.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: Account-Ban / Banned-Keyword Detection
|
||||
---
|
||||
|
||||
# Account-Ban / Banned-Keyword Detection
|
||||
|
||||
OmniRoute scans upstream error responses for signals that indicate a provider
|
||||
**account is permanently dead** (suspended / deactivated / ToS-banned) and, when
|
||||
matched, moves that connection into a **terminal `banned` state** so it is no
|
||||
longer selected for requests. This is what the **Security → Banned Keywords**
|
||||
settings card configures ("Additional keywords that trigger permanent account
|
||||
ban detection. Built-in keywords always apply.").
|
||||
|
||||
This page documents the built-in list, the detection flow, its scope, how to add
|
||||
custom keywords safely, and how to recover a flagged connection. The terminal
|
||||
state itself is part of the resilience model — see
|
||||
[RESILIENCE_GUIDE](../architecture/RESILIENCE_GUIDE.md) ("Terminal states").
|
||||
|
||||
**Source of truth:** `open-sse/services/accountFallback.ts`
|
||||
(`ACCOUNT_DEACTIVATED_SIGNALS`, `getMergedBannedSignals()`, `isAccountDeactivated()`).
|
||||
|
||||
## Built-in keywords
|
||||
|
||||
These 8 substrings always apply (case-insensitive), regardless of any custom list:
|
||||
|
||||
```
|
||||
account_deactivated
|
||||
account has been deactivated
|
||||
account has been disabled
|
||||
your account has been suspended
|
||||
this account is deactivated
|
||||
verify your account to continue (Antigravity / Google Cloud Code)
|
||||
this service has been disabled in this account for violation (Antigravity)
|
||||
this service has been disabled in this account (Antigravity)
|
||||
```
|
||||
|
||||
> This list evolves as providers change their ban wording. The authoritative
|
||||
> copy is `ACCOUNT_DEACTIVATED_SIGNALS` in `open-sse/services/accountFallback.ts`;
|
||||
> treat the block above as a snapshot.
|
||||
|
||||
Two adjacent, **separate** signal tables live in the same file and are *not* part
|
||||
of banned-keyword detection:
|
||||
|
||||
- `CREDITS_EXHAUSTED_SIGNALS` — billing/quota depleted (`insufficient_quota`,
|
||||
`credit_balance_too_low`, `payment required`, …) → terminal `credits_exhausted`.
|
||||
- `OAUTH_INVALID_TOKEN_SIGNALS` — **non-terminal**; a token refresh can recover.
|
||||
|
||||
Note: common transient phrases like **`rate limit`** / `429` are handled by the
|
||||
rate-limit / connection-cooldown path and are **not** ban signals.
|
||||
|
||||
## Detection flow
|
||||
|
||||
```
|
||||
upstream error response
|
||||
→ body stringified + lowercased
|
||||
→ isAccountDeactivated(body): getMergedBannedSignals().some(sig => body.includes(sig)) [substring match]
|
||||
→ match?
|
||||
→ connection testStatus = "banned" (permanent — 1-year cooldown, never auto-recovers)
|
||||
→ if setting `autoDisableBannedAccounts` is on → also isActive = false
|
||||
→ connection is skipped during account selection (combo QUOTA_BLOCKING statuses)
|
||||
```
|
||||
|
||||
- The match is a **case-insensitive substring** search on the response **body**
|
||||
(`isAccountDeactivated`, `accountFallback.ts`).
|
||||
- The permanent `banned` terminalization fires on a banned-signal body at **any
|
||||
HTTP status** (via `markAccountUnavailable` → `checkFallbackError`). The
|
||||
narrower **`deactivated`** label (`isActive=false` when the connection has no
|
||||
spare API keys) is written by the inline `chatCore.ts` path on **HTTP 401 / 403**
|
||||
(classified via `classifyProviderError` → `ACCOUNT_DEACTIVATED`). Note the
|
||||
`markAccountUnavailable()` path writes a *different* terminal status —
|
||||
**`expired`** — for the same `ACCOUNT_DEACTIVATED` signal (via
|
||||
`resolveTerminalConnectionStatus`), so the same ban can surface as either
|
||||
`deactivated` or `expired` depending on which path handled the response. (The
|
||||
older code comment says "when a 401 body contains these strings" — that
|
||||
understates the current behavior.)
|
||||
- A `banned` connection is excluded from selection everywhere terminal statuses
|
||||
are filtered (`isTerminalConnectionStatus`, combo `QUOTA_BLOCKING_CONNECTION_STATUSES`).
|
||||
|
||||
## Scope — which providers are scanned
|
||||
|
||||
**All providers.** The check runs in the generic error-handling pipeline that
|
||||
every failed upstream request flows through — it is **not** gated to
|
||||
OAuth/subscription scrapers. The resulting terminal state is per **connection**,
|
||||
not per provider.
|
||||
|
||||
That said, the built-in *strings* are oriented toward subscription/OAuth
|
||||
providers with real ban risk (ChatGPT Web, Claude Web, Codex, Muse Spark,
|
||||
Antigravity). An API-key provider will only trip the detector if its error body
|
||||
literally contains one of the substrings.
|
||||
|
||||
## Custom banned keywords
|
||||
|
||||
Add or remove keywords in **Security → Banned Keywords** (persisted as the global
|
||||
`customBannedSignals` setting via `PATCH /api/settings`). They are **added to**
|
||||
the built-in list — never a replacement — and hot-reload on save (and at startup)
|
||||
via `setCustomBannedSignals()`. Each keyword is capped at 200 characters; there is
|
||||
no array-length limit.
|
||||
|
||||
**⚠ False-positive risk — choose specific phrases.** Detection is a raw substring
|
||||
match on the whole response body, and a match is **permanent** (1-year cooldown,
|
||||
manual recovery). A broad keyword can ban a perfectly healthy connection:
|
||||
|
||||
- **Bad:** `quota`, `limit`, `error`, `denied` — appear in many transient errors.
|
||||
- **Good:** full ban sentences, e.g. `your account has been suspended for`,
|
||||
`account permanently banned`, `violation of our terms`.
|
||||
|
||||
Prefer the longest unambiguous phrase the provider returns on a real ban. When in
|
||||
doubt, watch the connection's `lastError` first, then add the exact wording.
|
||||
|
||||
## Recovering a flagged connection
|
||||
|
||||
Terminal `banned` / `deactivated` states **never auto-recover** (they are excluded
|
||||
from the proactive-recovery tick — only `unavailable` cooldowns recover on their
|
||||
own). An operator must clear them explicitly:
|
||||
|
||||
1. **Re-test the connection** — the dashboard **Test** action
|
||||
(`POST /api/providers/{id}/test`); a successful probe resets `testStatus` to
|
||||
`active` and clears the error fields.
|
||||
2. **Re-authenticate / edit credentials** — for OAuth providers, re-run the login
|
||||
/ refresh flow; provider create/import routes set `isActive = true`.
|
||||
3. **Re-enable the connection** — if `autoDisableBannedAccounts` set
|
||||
`isActive = false`, toggle it back on after fixing the account.
|
||||
|
||||
There is no separate "clear ban flag" button — recovery is re-test, re-auth, or
|
||||
re-enable, matching the general terminal-state rule in
|
||||
[RESILIENCE_GUIDE](../architecture/RESILIENCE_GUIDE.md).
|
||||
|
||||
## Source files
|
||||
|
||||
| Concern | File |
|
||||
| --- | --- |
|
||||
| Signal tables + match | `open-sse/services/accountFallback.ts` |
|
||||
| Terminalization / persistence | `src/sse/services/auth.ts` (`markAccountUnavailable`, `resolveTerminalConnectionStatus`, `clearAccountError`) |
|
||||
| Inline classification | `open-sse/handlers/chatCore.ts`, `open-sse/services/errorClassifier.ts` |
|
||||
| Terminal-state recovery exclusion | `src/lib/quota/connectionRecovery.ts` |
|
||||
| Custom-keyword runtime load | `src/lib/config/runtimeSettings.ts` (`setCustomBannedSignals`) |
|
||||
| Settings UI | `src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx` |
|
||||
@@ -5,6 +5,7 @@
|
||||
"PUBLIC_CREDS",
|
||||
"ERROR_SANITIZATION",
|
||||
"ROUTE_GUARD_TIERS",
|
||||
"BAN_DETECTION",
|
||||
"STEALTH_GUIDE",
|
||||
"EGRESS_POLICY",
|
||||
"COMPLIANCE",
|
||||
|
||||
Reference in New Issue
Block a user