Skip to content

[front] Support fixed windows in the plan fair-use AWU limiter - #29617

Open
tdraier wants to merge 1 commit into
rate-limiter-fixed-windowfrom
plan-fair-use-fixed-window
Open

[front] Support fixed windows in the plan fair-use AWU limiter#29617
tdraier wants to merge 1 commit into
rate-limiter-fixed-windowfrom
plan-fair-use-fixed-window

Conversation

@tdraier

@tdraier tdraier commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Description

POC — may not merge. Explores extending the plan-level fair-use AWU limiter (plan.limits.assistant.maxAwuCredits / maxAwuCreditsTimeframe) to support fixed-window (calendar) timeframes in addition to rolling ones, so a plan's fair-use cap can reset on a clock boundary.

  • MAX_AWU_CREDITS_TIMEFRAMES = rolling + FIXED_AWU_CREDITS_TIMEFRAMES (calendar day/week/month). The contract_billing_cycle window is not offered for plans (it's specific to the per-user spend cap in [front] Rate-limiter backup for the per-user spend cap #29576).
  • getFairUseAwuCreditsCount / recordFairUseAwuCredits dispatch to the rolling limiter or the fixed-window (calendar) counter based on the timeframe; enforcement (conversation), recording (credit_cost), status (user_block) and reset use them.

Stacked on #29575; sibling of the spend-cap PR #29576.

Tests

  • fair_use_window.test.ts — round-trip for a rolling timeframe and a calendar (fixed) timeframe via the dispatch helpers (real Redis).
  • tsgo --noEmit clean on front and front-api.

Risk

Low-to-medium, but currently no workspace uses the fair-use limiter (every plan ships maxAwuCredits = -1), so this touches a dormant path. Rolling timeframes keep their exact previous behavior; only the dispatch layer is new. No schema/data changes — safe to roll back.

Deploy Plan

Merge #29575 first. No migration/config. To try it: in poke set a plan's maxAwuCreditsTimeframe to calendar_day / calendar_week / calendar_month and a non--1 maxAwuCredits, then exercise the fair-use limit on a workspace on that plan. (Being a POC, likely stays open for evaluation rather than merging.)

🤖 Generated with Claude Code

@tdraier

tdraier commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
playground Ignored Ignored Preview Jul 28, 2026 8:43pm
storybook Ignored Ignored Preview Jul 28, 2026 8:43pm

Request Review

@dust-agent dust-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coding rules review for PR #29617.

Comment thread front/lib/api/assistant/rate_limits.ts Outdated
workspaceId: string,
timeframe: FixedAwuCreditsTimeframeType
): Promise<FixedWindowBounds> {
if (timeframe === "contract_billing_cycle") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[GEN6] Prefer exhaustive switch + assertNever over if/else on union types

FixedAwuCreditsTimeframeType is a string union. Branching on it with if/else instead of an exhaustive switch + assertNever means TypeScript won't enforce that all future values are handled. Please use a switch so any new fixed timeframe added to the union is a compile error here.

Suggested change
if (timeframe === "contract_billing_cycle") {
switch (timeframe) {
case "contract_billing_cycle": {
const periodResult =
await getCachedMetronomeCurrentBillingPeriod(workspaceId);
if (periodResult.isOk() && periodResult.value) {
const { cycleStart, cycleEnd } = periodResult.value;
return {
label: `cycle-${cycleStart.getTime()}`,
windowEndMs: cycleEnd.getTime(),
};
}
return computeCalendarWindowBounds("calendar_month", new Date());
}
default:
return computeCalendarWindowBounds(timeframe, new Date());
}

key: string;
timeframe: MaxAwuCreditsTimeframeType;
}): Promise<Result<number, Error>> {
if (isRollingAwuCreditsTimeframeType(timeframe)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[GEN6] Prefer exhaustive switch + assertNever over if/else on union types

Branching on MaxAwuCreditsTimeframeType via isRollingAwuCreditsTimeframeType + implicit else provides no compile-time guarantee that all non-rolling values are accounted for. If a new timeframe is added that is neither rolling nor fixed, it silently falls into the fixed-window path. Please prefer an exhaustive switch with assertNever on the full union so TypeScript enforces exhaustiveness.

@tdraier
tdraier force-pushed the rate-limiter-fixed-window branch from 52a1d1d to 548d25d Compare July 28, 2026 14:26
@tdraier
tdraier force-pushed the plan-fair-use-fixed-window branch from 8373076 to d8dd543 Compare July 28, 2026 14:27
@tdraier
tdraier force-pushed the rate-limiter-fixed-window branch from 548d25d to 4ebb8a1 Compare July 28, 2026 14:42
@tdraier
tdraier force-pushed the plan-fair-use-fixed-window branch from d8dd543 to 936c6fb Compare July 28, 2026 14:44
@tdraier
tdraier force-pushed the rate-limiter-fixed-window branch from 4ebb8a1 to 8ac32a4 Compare July 28, 2026 15:24
@tdraier
tdraier force-pushed the plan-fair-use-fixed-window branch 2 times, most recently from 58fd074 to 07806a8 Compare July 28, 2026 15:45
@zeropath-ai

zeropath-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🟡 1 security issue detected below the blocking threshold. Reviewed everything up to 812c61a.

The following issues were found:

  • Issue 1: Broken Access Control
    • Location: front/lib/api/assistant/rate_limits.ts:184-200
    • Score: MEDIUM (50.0)
    • Description: The new fair-use AWU recording path for fixed-window timeframes silently skips recording usage when the contract billing period cannot be resolved. Because the pre-message limit check also returns an error on the same condition, the conversation code treats the error as non-blocking and only logs it. An attacker who can trigger messages while contract_billing_cycle resolution fails can continue sending assistant messages without the AWU usage being accumulated against the plan limit.

Evidence: In recordFairUseAwuCredits, fixed-window recording returns early on resolver errors: logger.error(..., &quot;[FairUse] Could not resolve fixed window; skipping usage record&quot;); return; at lines 188-193. In conversation.ts, the corresponding read path logs Failed to read fair-use AWU credits rate limit. when getFairUseAwuCreditsCount returns an error, but does not enforce the limit or reject the request. This means the enforcement path fails open whenever getCachedMetronomeCurrentBillingPeriod(workspaceId) cannot resolve the current billing period.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► front/lib/api/assistant/conversation.ts
    Switch to getFairUseAwuCreditsCount for AWU credits read
► front/lib/api/assistant/credit_cost.ts
    Use recordFairUseAwuCredits and related rate limit helpers instead of direct rate limiter calls
► front/lib/api/assistant/fair_use_window.test.ts
    Add tests for fair-use AWU window dispatch (rolling vs fixed)
Enhancement ► front/lib/api/assistant/rate_limits.ts
    Expose getFairUseAwuCreditsCount and recordFairUseAwuCredits; integrate fixed-window and rolling window handling; add expire logic for calendar windows
Enhancement ► front/lib/client/credits.ts
    Format AWU timeframes to include calendar day/week/month mappings
Enhancement ► front/lib/metronome/user_block.ts
    Import and use getFairUseAwuCreditsCount alongside makeFairUseAwuCreditsRateLimitKeyForUser
Enhancement ► front/lib/utils/fixed_window.test.ts
    Add tests for fixed-window calendar behavior (computeCalendarWindowBounds)
Enhancement ► front/lib/utils/rate_limiter.ts
    Add calendar window support; adjust timeframe handling to support rolling and fixed windows; implement computeCalendarWindowBounds and related helpers
Enhancement ► front/types/plan.ts
    Include CALENDAR_AWU_CREDITS_TIMEFRAMES and ROLLING_AWU_CREDITS_TIMEFRAMES; define FIXED_AWU_CREDITS_TIMEFRAMES and MAX_AWU_CREDITS_TIMEFRAMES; expose FixedAwuCreditsTimeframeType
Enhancement ► front/types/rate_limiter.ts
    Add types for rolling and calendar AWU timeframes and utility isRollingAwuCreditsTimeframeType

Comment thread front/lib/api/assistant/rate_limits.ts Outdated
Comment on lines +184 to +200
const boundsResult = await resolveFairUseFixedWindowBounds(
workspaceId,
timeframe
);
if (boundsResult.isErr()) {
logger.error(
{ workspaceId, timeframe, err: boundsResult.error },
"[FairUse] Could not resolve fixed window; skipping usage record"
);
return;
}
await addFixedWindowCount({
key,
bounds: boundsResult.value,
incrementBy,
logger,
});

@zeropath-ai zeropath-ai Bot Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authorization: AWU rate-limit skip on billing period resolve failure (front/lib/api/assistant/rate_limits.ts) (Severity: MEDIUM)

The vulnerability allows unlimited messages if the billing period cannot be resolved, as the fixed-window AWU recording skips usage and the pre-check treats the error as non-blocking. This leads to open enforcement when getCachedMetronomeCurrentBillingPeriod fails, enabling an attacker to bypass fair-use limits by triggering messages while resolution fails, which causes usage not to be accumulated against the plan.
View details in ZeroPath

Automatic patch generation was not possible for this finding.

View reasoning


💬 Reply @ZeroPath false-positive because … or @ZeroPath accepted-risk because … to triage this finding, or ask it any question.

All commands

@tdraier
tdraier force-pushed the rate-limiter-fixed-window branch from 8ac32a4 to b5d6616 Compare July 28, 2026 16:37
@tdraier
tdraier force-pushed the plan-fair-use-fixed-window branch from 07806a8 to 64a93f5 Compare July 28, 2026 16:37
@tdraier
tdraier force-pushed the rate-limiter-fixed-window branch from b5d6616 to 8b74e44 Compare July 28, 2026 16:40
@tdraier
tdraier force-pushed the plan-fair-use-fixed-window branch from 64a93f5 to cb38acf Compare July 28, 2026 16:40
POC (may not merge). Extends the plan-level AWU-credits fair-use limiter
to support calendar fixed windows in addition to rolling ones.

Owns the rate-limiter window vocabulary this needs, kept out of the base
fixed-window primitive since only fair-use consumes it:
- types/rate_limiter.ts: ROLLING vs CALENDAR timeframe groupings +
  isRollingAwuCreditsTimeframeType.
- computeCalendarWindowBounds (UTC day/ISO-week/month boundaries) and the
  RollingAwuCreditsTimeframeType narrowing of getTimeframeSecondsFromLiteral.
- MAX_AWU_CREDITS_TIMEFRAMES gains the calendar_* variants; callers route
  rolling vs calendar via isRollingAwuCreditsTimeframeType.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tdraier
tdraier force-pushed the rate-limiter-fixed-window branch from 8b74e44 to 9163a2e Compare July 28, 2026 20:40
@tdraier
tdraier force-pushed the plan-fair-use-fixed-window branch from cb38acf to 812c61a Compare July 28, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant