You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* fix(chat): bound deployed-chat callers and stop leaking chat gate config
Two authorization/throttling defects on chat deployments.
**Denial of wallet on POST /api/chat/[identifier].** A deployed chat resolves
its execution principal from the workflow's workspace, so the plan rate bucket,
the usage/credit check and the concurrency reservation all belong to the owner
while the request belongs to whoever found the link. Nothing bounded the caller,
and an abort refunds none of it. Both the per-IP and the per-deployment bucket
now run after auth and before `preprocessExecution`, on every execution
regardless of `authType` — an email or SSO visitor is still not the payer.
`GET /api/chat/validate` answered for any anonymous caller, so `available:false`
inventoried live deployments; it now needs a session and a per-user bucket.
**Chat gate config exposed at workflow `read` on GET /api/workflows/[id]/chat/
status.** The route reimplemented the admin-gated detail projection inline,
serving the `allowedEmails` allow-list, `hasPassword` and the customization blob
to any workspace viewer, and asserting no `deploy.chat` capability. It is now an
adapter over `chat_deployments.list` — the same operation `GET /api/v2/chat-
deployments` binds — returning only the deployment's id and identifier, which is
all the editor reads before fetching the detail from `/api/chat/manage/{id}`.
The two buckets are the existing `enforceIpRateLimitWithIndependentBackstop`
plus a new `enforceResourceRateLimit` beside its siblings in `route-helpers`.
The IP bucket is consulted first and returns on refusal, so one flooding IP
cannot drain the deployment's budget and 429 the real audience with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj
* fix(chat): drop the env knobs and put the ceiling under the plan bucket
Two corrections to the execution throttle.
The per-deployment ceiling was 300/min, at or above the workspace `sync`
counter it debits on every plan but enterprise — 50 free, 150 pro, 300 team.
A flood therefore drained that shared counter, which the owner's API, webhook
and scheduled runs draw from too, before the ceiling ever refused: the
availability half of the report went unmitigated on exactly the plans most
workspaces are on. It is now 60/min sustained, under even the cheapest paid
plan, with a test that pins it there against `RATE_LIMITS`. The per-IP bucket
drops to 30/min so one host cannot take a deployment's whole allowance, and
both gain the 2x burst allowance the plan buckets already use.
Both limits go back to plain constants. Every sibling deployment throttle —
password, OTP, SSO, on chat and on public file shares — is a hardcoded
`TokenBucketConfig`, so the two env vars were the only configurable ones of
their kind and bought speculative tuning for a control with sane defaults.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj
* fix(chat): derive the chat ceiling from the plan table it must stay under
The 60/min ceiling still sat above the free plan's 50/min sync rate, so on free
the shared counter — the one the owner's API, webhook and scheduled runs also
draw from — still emptied before the ceiling refused. Every plan rate is also
operator-overridable through `RATE_LIMIT_*_SYNC`, which no hardcoded number can
track.
It is now derived: 80% of the smallest configured plan sync rate, which is
40/min with the defaults and stays under every plan by construction. The per-IP
bucket follows at half that. Tests assert the invariant against each plan in
`RATE_LIMITS`, on burst as well as sustained rate, rather than pinning numbers
that would need editing the next time a plan default moves.
This floor is shared by all plans, so enterprise is held to the same 40/min as
free. Sizing the slice to the payer's own plan needs the subscription, which
`preprocessExecution` resolves just after this runs — that is the follow-up,
and the same hook bounds the generic-webhook surface that is still unbounded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj
* docs(chat): note the one plan rate where the derived ceiling lands equal
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj
* refactor(rate-limit): scope the per-IP bucket by resource id, not by bucket name
The chat call interpolated the deployment id into `bucketName`, which produces
a correct key but puts a per-deployment value into the field both log lines
emit as `bucket` — high cardinality on a label meant to name a bucket family,
and asymmetric with the `enforceResourceRateLimit` call beside it that takes
the id as its own argument.
`enforceIpRateLimitWithIndependentBackstop` now takes an optional `resourceId`,
so the pair reads the same way and `resourceId` is logged as its own field.
The unscoped key shape is unchanged for the existing callers, with a test
pinning both shapes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj
* test(rate-limit): drop needless any casts on the mock request
createMockRequest already returns NextRequest, so the casts weakened the
helper's input contract in the new tests for nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
0 commit comments