fix(auth+billing): steward auth sweep, dynamic AI pricing catalog, privacy hardening#458
fix(auth+billing): steward auth sweep, dynamic AI pricing catalog, privacy hardening#458
Conversation
bearerToken was referenced before its const declaration (temporal dead zone), causing a ReferenceError that crashed the middleware and redirected to /auth/error on every steward-authenticated request. Co-authored-by: wakesync <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
test comment placeholder |
|
Code Review overview test |
|
Code Review - Overview - This PR fixes a redirect loop for steward users. The proxy.ts fix is correct. Issues: 1) authReady omits stewardAuthenticated; 2) fragile cookie substring match; 3) single-run useEffect assumption. Full review to follow. |
Code Review |
ChangelogAll notable engineering changes to this repository are recorded here. For product-facing release notes on the docs site, see The format is based on Keep a Changelog. [Unreleased]Added
Changed
Documentation
|
|
test |
Code Review test |
Code ReviewOverviewThis PR fixes a redirect loop for steward users in the dashboard layout. Steward users authenticate via a steward-token cookie rather than Privy, so the client-side auth check was redirecting them to /login even though server-side auth was valid. Two changes:
The proxy.ts fix is correct and necessary. The layout fix works but has a few concerns. Issues1. authReady does not account for steward auth (functional bug) layout.tsx line 81: If a steward user lands on the dashboard, stewardAuthenticated will be true but Privy's ready may still be false. The component shows a loading spinner until Privy finishes initialising, which can take several seconds or fail in steward-only environments. Fix: 2. Fragile cookie substring match document.cookie is a semicolon-separated string. This check passes for any cookie whose name contains steward-token (e.g. my-steward-token=abc). Prefer: 3. Single-run useEffect is fragile The empty dependency array means the check only runs on mount. If the steward cookie is written after mount, auth will not be detected without a full page reload. This works for the current flow where the cookie is set before navigating to /dashboard, but a comment documenting that assumption would help. Minor notes
SummaryThe proxy.ts change is a clean fix. The layout change resolves the redirect loop, but issue 1 (authReady omission) is the most impactful: steward users will likely still see a prolonged loading spinner waiting on Privy initialisation. Issues 2 and 3 are lower priority but worth a follow-up for robustness. |
The dashboard layout's client-side auth check only looked at Privy's usePrivy().authenticated. Steward users have a steward-token cookie but no Privy session, causing a redirect loop. Now checks for steward-token cookie alongside Privy auth. Co-authored-by: wakesync <[email protected]>
7269fe7 to
9cb5989
Compare
Code Review — PR #458: fix: dashboard layout accepts steward auth (cookie check)OverviewThis PR fixes a redirect loop for steward users by introducing a two-cookie pattern: the existing The proxy.ts change moves Issues
// app/dashboard/layout.tsx
setStewardAuthenticated(
document.cookie.split(";").some((c) => c.trim().startsWith("steward-authed="))
);This is consistent with how Steward check fires only on mount (empty deps array) useEffect(() => {
setStewardAuthenticated(document.cookie.includes("steward-authed="));
}, []);If a steward session is established (or revoked) while the user is on the dashboard — without a full page reload — the UI state goes stale. The API will enforce auth correctly server-side, but the client-side guard won't react. Consider listening for the Cookie desync window
Positives
Minor
SummaryThe core approach is sound. The main actionable items before merge:
|
- treat steward auth as signed-in for user menu, sidebar CTA, and locked nav items - fetch server profile/org balance for steward-backed sessions - clear steward session on sign out Co-authored-by: wakesync <[email protected]>
- add shared hybrid session auth hook for privy + steward - update landing, invite, payment success, admin, chat, and settings surfaces - keep generic session-aware UI from rendering logged-out states for steward users - preserve privy-specific flows where bearer token login is still intentional Co-authored-by: wakesync <[email protected]>
|
pushed a broader follow-up sweep on this branch. new commits:
what was added beyond the original dashboard layout fix:
kept privy-specific flows alone where they still intentionally depend on privy bearer-token behavior. |
Code ReviewOverviewThis PR fixes a redirect loop for steward users in the dashboard by introducing a unified IssuesHigh Priority1. Inconsistent use of the new abstraction
These should be refactored to use 2. const { user } = useSessionAuth();
const userId = user?.id || "";
3. Moving if (stewardToken && !privyToken && !bearerToken) {This is correct post-move, but the original code had a subtle bug where Medium Priority4.
setStewardAuthenticated(document.cookie.includes("steward-authed="));Any page script (or the user in DevTools) can set 5. The layout file reads the cookie directly instead of calling 6. Credits display may show "Loading…" indefinitely for steward sessions {loadingCredits && creditBalance === null && userProfile?.organizationCreditBalance == null ? (
<Loader2 .../>For steward sessions, Low Priority7. (authSource === "steward" && user && "walletAddress" in user ? user.walletAddress : undefined)This works today but is fragile — it relies on a runtime property check rather than a typed interface. If 8. The 9. No tests The core fix ( SummaryThe fix correctly identifies the problem and the |
…tion Co-authored-by: wakesync <[email protected]>
Code Review: PR #458 — fix: dashboard layout accepts steward auth (cookie check)OverviewThis PR fixes a redirect loop for steward users who authenticate via Bugs / Issues1. The original code referenced 2. The root component that motivated the entire PR still has its own inline steward auth check instead of using the new hook: // layout.tsx (as written)
const [stewardAuthenticated, setStewardAuthenticated] = useState(false);
useEffect(() => {
setStewardAuthenticated(document.cookie.includes("steward-authed="));
}, []);This means there's a guaranteed flash: on first render Using 3. This check matches any value for the cookie — Inconsistency: Not all components use
|
Co-authored-by: wakesync <[email protected]>
Code Review — PR #458: fix: dashboard layout accepts steward auth (cookie check)OverviewThis PR fixes a redirect loop for steward users (who authenticate via 🚨 Critical: Security Issue in
|
approve-pr.yml |
Remove or fix (command injection + unrelated scope) |
dashboard/layout.tsx |
Use useSessionAuth instead of manual cookie read |
SidebarBottomPanel, SidebarItem |
Use useSessionAuth for consistency |
useAdmin wallet fallback |
Tighten type narrowing |
proxy.ts move |
Good fix — note it in the commit message |
Core useSessionAuth hook |
Solid — approve once above are addressed |
Eliza Cloud V2Stack
Commandsbun install # Install dependencies
bun run dev # Start dev server
bun run build # Production build
bun run db:migrate # Apply database migrations
bun run db:generate # Generate migration from schema
bun run db:studio # Open Drizzle StudioDatabase MigrationsNever use Schema Change Workflow
Custom Migrationsnpx drizzle-kit generate --custom --name=descriptive_nameRules
Type Checking
bun run check-types 2>&1 | grep -E "(your-file\\.ts|your-other-file\\.ts)"If the grep returns empty, your changes are clean. Project Structure |
|
test |
Code Review — PR #458: fix: dashboard layout accepts steward auth (cookie check)OverviewThis PR fixes the dashboard layout redirect loop for steward users, but it bundles a large set of unrelated changes (54K+ line diff, mostly Drizzle snapshot JSON). The core auth fix is small and reasonable; the scope makes safe review harder. 🚨 Critical: Remove
|
| Block | approve-pr.yml — branch protection bypass + shell injection |
| Fix | dashboard/layout.tsx — fragile cookie match + one-shot stale state |
| Fix | Admin pricing route — unhandled ZodError (return 400 not 500) |
| Concern | useStewardAuth violates React hook rules |
| OK | Migrations are clean and correctly targeted |
| OK | Non-httpOnly steward-authed cookie pattern is intentional and fine |
Co-authored-by: wakesync <[email protected]>
…Cache
Previous commits attempted to rename but the disk file still had a
hyphen in the identifier ("third-party-catalog-cache"), which TypeScript
parses as subtraction. This sed-based fix writes the actual replacement.
Co-authored-by: wakesync <[email protected]>
Code ReviewOverviewThe stated purpose of this PR is a targeted fix for the dashboard layout redirect loop affecting steward-auth users (no Privy session). However, the actual diff is 54K additions / 1.3K deletions spanning auth flows, DB migrations, AI pricing infrastructure, UI components, new packages, and GitHub workflow changes. The size makes this very hard to review with confidence. 🚨 Critical – Security1. This workflow allows any repository collaborator with run: gh pr review ${{ inputs.pr_number }} --approve --body "Approved via automated workflow dispatch."This creates a mechanism to self-approve PRs or bypass required-reviewer policies on demand. It should not be merged and should be removed before this PR lands. 2. OAuth tokens passed as URL query parameters ( The OAuth callback returns tokens in plain query params (
Prefer a short-lived
|
|
Code Review - PR 458 The stated fix (steward cookie auth in dashboard layout) is correct. However, this PR bundles 4+ unrelated changes into a 54k-line diff. CRITICAL ISSUES:
This workflow adds a workflow_dispatch trigger letting anyone with repo write access approve any PR. The pr_number input is interpolated directly into a shell command without quoting - a command injection risk. More critically, automating PR approval undermines branch protection rules and is a CI/CD supply chain attack vector. Recommendation: Remove entirely. If needed, create a dedicated PR with quoted/validated input and actor restrictions.
CLAUDE.md requires: Use IF NOT EXISTS for creating tables. Both ai_pricing_entries and ai_pricing_refresh_runs tables are created without this guard. A re-run will fail in production with "table already exists."
Old routes had withRateLimit(handlePOST, RateLimitPresets.STANDARD). New re-exports delegate to v1 routes that export async function POST directly without rate limiting. Consolidation is architecturally correct, but v1 routes need equivalent rate limiting before this ships. Affected: /api/elevenlabs/stt, /api/elevenlabs/tts, /api/elevenlabs/voices/clone.
Title says "fix: dashboard layout accepts steward auth (cookie check)" but the PR also includes: new AI pricing catalog (DB tables, schema, repository, service, admin API, cron job), voice route consolidation (~800 lines deleted/moved), video model list update (veo3.1, kling v3, hailuo-2.3), new billing tracking fields (billingSource, platformMarkup, baseTotalCost), new use-session-auth.ts hook, music-metadata plus ~10 transitive packages. Billing/pricing changes cannot be adequately reviewed when bundled with an unrelated auth fix. MEDIUM ISSUES:
POSITIVE:
Summary: Cannot approve as-is. Blockers: (1) remove approve-pr.yml, (2) add IF NOT EXISTS to migration 0064, (3) confirm v1 voice routes have rate limiting. Recommend also splitting the billing/pricing/model changes into focused PRs. |
Keeps our 0065_add_generations_is_public at idx 62 and develop's 0067_allow_multiple_oauth_connections_per_user at idx 63. Co-authored-by: wakesync <[email protected]>
Code Review — PR #458: fix dashboard layout accepts steward auth (cookie check)OverviewThe described fix (preventing redirect loops for steward users in the dashboard) is straightforward and correct. However, this PR bundles several unrelated concerns — some of which are significant enough to block merging. 🚨 Critical Issues1.
|
| Core steward redirect fix | ✅ Correct |
proxy.ts bug fix (TDZ) |
✅ Correct, important |
approve-pr.yml workflow |
🚨 Remove — security risk |
PRIVY_MIGRATION_TRIAGE.md |
❌ Remove from repo |
| Removed anonymous image gen | |
| Video partial charge | |
| Duplicated JWT decode logic |
Recommendation: Extract the core dashboard-layout fix and the proxy.ts bug fix into a focused PR. The remaining items (anonymous auth removal, video billing change, StewardProvider refactor) should be separate PRs with proper descriptions. The approve-pr.yml workflow should be dropped entirely.
The workflow allowed anyone with workflow_dispatch access to approve any
PR number via GITHUB_TOKEN, bypassing branch protection and normal review.
Additionally had a shell injection vulnerability via unquoted
${{ inputs.pr_number }} interpolation.
This was bundled into PR 458 but unrelated to the steward auth fix.
Reviewers flagged it as a critical supply-chain risk.
Co-authored-by: wakesync <[email protected]>
Code ReviewOverviewThis PR does significantly more than its title suggests. Beyond the dashboard redirect-loop fix, it includes:
Issues🔴 High Priority
export function useStewardAuth() {
try {
return useStewardAuthRaw(); // ← hook inside try/catch
} catch {
return STEWARD_AUTH_FALLBACK;
}
}React requires hook call order to be unconditionally stable across renders. A try/catch can mask state inconsistencies and may break under React 18 concurrent rendering. The biome-ignore comment acknowledges this but it's still unsafe. The correct fix is to check whether the provider is mounted via a context sentinel or
const [stewardAuthenticated, setStewardAuthenticated] = useState(false);
useEffect(() => {
setStewardAuthenticated(document.cookie.includes("steward-authed="));
}, []);The Recommendation: replace the two-state approach with Removing anonymous image generation is an undocumented breaking change The PR description mentions a cookie-check fix. It does not mention that anonymous users can no longer generate images. Before, 🟡 Medium PriorityJWT decode logic is duplicated across 3 files
localStorage JWT trusted for UI auth gating without signature verification
OAuth callback doesn't handle setSessionCookie(token).then(() => {
window.location.href = getSafeReturnTo(searchParams);
});If
authInstanceRef.current = new StewardAuth({ baseUrl: apiUrl, ... });
Partial billing: 10% is a magic number with no configuration await reservation.reconcile(Math.ceil(quotedVideoCost.totalCost * 0.1 * 1e6) / 1e6);The 10% figure appears three times with no named constant and no comment explaining the rationale (e.g. "fal.ai charges for compute even on failed generations"). The 🟢 Low Priority
SummaryThe auth bridging work in |
1. Remove PRIVY_MIGRATION_TRIAGE.md - planning docs shouldn't be in repo 2. dashboard/layout.tsx now uses useSessionAuth() - eliminates the manual cookie check race condition and duplicated auth logic. The steward-authed cookie-based check ran only once on mount; useSessionAuth subscribes to storage events and steward-token-sync events for reactive updates. 3. useStewardAuth() no longer violates Rules of Hooks - reads StewardAuthContext directly via useContext instead of try/catching useAuth(). Same behavior, no hook rule violation. 4. Migration 0064 now uses IF NOT EXISTS on tables and indexes per CLAUDE.md migration rules (prevents re-run failures in prod). Co-authored-by: wakesync <[email protected]>
Addressed review feedbackFollowing up on the Claude reviews. Changes in commit Fixed
Scope clarifiedTitle updated to reflect that this PR legitimately bundles:
Splitting isn't feasible now — #455 is stacked under this branch and all three touch overlapping files (proxy.ts, pricing routes, generations table). If review needs a smaller surface, happy to rebase differently. Known remaining concerns (not blocking)
Production context
|
Code Review — PR #458: dashboard layout accepts steward auth (cookie check)OverviewThis PR fixes a redirect loop for Steward-authenticated users by introducing a unified The core auth fix is sound and the approach is reasonable. A few concerns worth addressing below. Security Concerns1. cookieStore.set("steward-authed", "1", { httpOnly: false, ... });This non-httpOnly cookie is used as the client-side signal for steward auth in 2. JWT decoded without signature verification 3. Public blob storage (documented but unresolved) Code Quality Issues4. JWT parsing logic duplicated in three places 5. 250ms polling hack in const t = setTimeout(handler, 250);This races against login completion timing. It works but is fragile — if login takes >250ms, the hook misses the first update and relies on the storage event. The comment should explain the failure mode this guards against. 6. Video partial charge uses fragile floating-point rounding Math.ceil(quotedVideoCost.totalCost * 0.1 * 1e6) / 1e6The multiply-by-1e6/divide-by-1e6 pattern is a common but unreliable way to round floats. For billing amounts, use const PARTIAL_CHARGE_RATE = 0.1;7. Missing CREATE INDEX "generations_is_public_idx" ON "generations" USING btree ("is_public") WHERE "is_public" = true;Per CLAUDE.md and the fix applied to 0064, indexes should use 8. Dual localStorage polling (redundant) 9. Stale 10. const authConfig = useMemo(() => ({ baseUrl: apiUrl }), [apiUrl]);
Positive Changes
SummaryThe auth fix is the right approach and the implementation is solid. The main asks before merging:
🤖 Generated with Claude Code |
The silent-logout bug: when an access token sat idle long enough to fully expire (browsers throttle setInterval in background tabs, so a 15-min idle was enough), the auto-refresh loop had already given up because it only ran when `secs > 0`. On top of that, syncToken would delete the server cookie as soon as it saw an expired token, killing any chance of server-side recovery. This refactor: - Drops the `secs > 0` gate in checkAndRefresh so we try to refresh even when the access token is fully expired (the refresh token is typically still valid for 30 days). - Moves the "clear server cookie" decision to AFTER refresh fails instead of firing on every expired-token observation. - Adds a visibilitychange listener so we immediately attempt a refresh when a tab becomes visible again (covers the 'came back from lunch' case where background setInterval was throttled). - Runs an eager refresh check on mount (covers hard reload after idle). Co-authored-by: wakesync <[email protected]>
Code Review — PR #458OverviewThis is a well-scoped stacked PR fixing three real problems: a steward auth redirect loop, a live pricing catalog (from PR #455 with patches), and a critical privacy leak in the explore gallery. The actual hand-written code delta is under ~1,000 lines; the rest is a Drizzle snapshot auto-generated file. 🔴 Critical / Bugs1. In setSessionCookie(token).then(() => {
window.location.href = getSafeReturnTo(searchParams);
});There's no 2. Duplicate JWT parsing across two files
3. Charging 10% for blob upload failure may be unfair In 🟡 Security4. The new non-
5. Vercel Blob public access (documented but deferred) The TODO in 🟡 Code Quality6. There are two suppressed dependency warnings for
7. The 250ms polling timer in const t = setTimeout(handler, 250);This "poll once more after a tick" pattern is a timing hack to handle the case where login just completed. It works, but it means every component mounting 8. User display logic in The display name/identifier/initials computation has grown into three parallel waterfall chains with 🟡 Migration / DB9. Migration journal numbering gap
10. The diff shows the fix (adding ✅ What's Good
No TestsNone of the new behavior (session hook, pricing catalog, Summary: The core changes are correct and well-reasoned. The three items I'd address before merge are: the missing |
0.6.5 didn't re-export StewardAuthContext from the package index, even though use-session-auth.ts imports it directly. Builds worked before because of lockfile drift; Turbopack is stricter. 0.6.6 adds the missing re-export. Co-authored-by: wakesync <[email protected]>
Code Review — PR #458Scope reviewed: auth sweep, pricing catalog patches, privacy hardening. OverviewSolid, well-scoped PR that fixes real bugs (redirect loop, TDZ crash, privacy leak) and hardens two subsystems. The change is large but justified — the auth sweep is mechanical and the pricing improvements are cohesive. Observations below from most to least critical. Security / Correctness[Major — privacy] [Moderate] [Moderate] OAuth tokens transiently appear in the browser URL [Low] Bug Fixes (Confirmed Correct)proxy.ts TDZ fix ✓
Pricing Catalog PatchesSeedance parser ✓ DB fallback on HTML parse failure ✓ Cache eviction ✓
10% partial charge on failed video generation Code QualityDuplicate JWT decode logic // lib/steward-jwt.ts
export function decodeStewardToken(token: string): { id: string; email: string; exp?: number; walletAddress?: string } | null { ... }
export function tokenSecsRemaining(token: string): number | null { ... }
export function tokenIsExpired(token: string): boolean { ... }
const t = setTimeout(handler, 250);This fires a localStorage re-read 250 ms after mount on every component that uses
const signInHref = useMemo(() => {
if (typeof window === "undefined") return "/login";
...
}, []);Empty dependency array means this only computes once. Migrations0064 (pricing catalog) — 0065 (
Summary
The blocking items are low severity — recommend merging after confirming:
|
Shadow reported still getting logged out of elizacloud. Traced the bug: when the client-side refresh loop hasn't fired yet (e.g. came back after server-rendered navigation with expired access cookie), the middleware redirects to /login. The login page calls `auth.getSession()` \u2014 which returns null if expired \u2014 and shows the login UI even though a 30-day refresh token is still sitting in localStorage. Now the login page tries `auth.refreshSession()` before falling back to the UI. If it succeeds, sets the cookie and redirects the user back to where they were going. Fast-path for the "valid session" case is preserved. Co-authored-by: wakesync <[email protected]>
Code ReviewOverviewThis PR bundles three scopes: Steward auth redirect loop fix (widespread Bugs Fixed — Confirmed Good
IssuesDuplicate JWT decoding logic
Double JSDoc block on There are two consecutive JSDoc blocks on the same function — an old one-liner that was left in alongside the replacement. The first block should be removed. Explore gallery will be empty after deploy
The new non-httpOnly Partial-charge arithmetic precision Math.ceil(quotedVideoCost.totalCost * 0.1 * 1e6) / 1e6This ceiling-rounds to 6 decimal places. When Minor Notes
SummaryThe privacy fix and TDZ/hooks fixes are high-value. Main asks before merge: consolidate the duplicate JWT parsing into a shared utility, remove the stale JSDoc, and file a follow-up ticket for the explore gallery becoming empty post-deploy. Everything else is polish. |
Summary
Stacked PR combining three related fixes that all touch the
/dashboardand API surface. Rebased ontodevelop, includes Shaw's live AI pricing work from PR #455.What's in here
1. Steward auth redirect loop (original scope)
useSessionAuth()hook to recognize both Privy and Steward sessions (fixes the redirect loop)useSessionAuth()so steward-only users see a correctly signed-in UI everywhereuseStewardAuth()usesuseContext(StewardAuthContext)instead of callinguseAuth()inside try/catch (no more Rules of Hooks violation)proxy.ts: hoistedbearerToken/authHeader/playwrightTestSessiondeclarations above the steward early-return (fixes a latent temporal dead zone bug wherebearerTokenwas referenced before itsconstdeclaration)2. Live AI pricing catalog (from Shaw's PR #455)
VIDEO_GENERATION_COST/IMAGE_GENERATION_COSTflat constants/api/v1/pricing/summaryendpoint for dashboard displayai_pricing_entries+ai_pricing_refresh_runstables (now withIF NOT EXISTS)3. Patches on top of Shaw's pricing code
unimplemented, would crash the refresh cron)/api/v1/pricing/summaryhas error handling per model — one failing fal endpoint no longer 500s the whole routethird-partyCatalogCacheevicts expired entries on write (was growing unbounded)4. Privacy / security hardening
is_publiccolumn togenerations(defaultfalse)listRandomPublicImages()now filters byis_public = true(was leaking ALL users' images to the explore/discover gallery)app/api/v1/generate-image/route.ts— authentication now required, returns 401 without it/api/v1/generate-imagewithout authaccess: "public"currently; proper fix (auth-gated proxy route) deferredContext on the ~26k line diff
Most of it (25,152 lines) is
packages/db/migrations/meta/0065_snapshot.json, which Drizzle auto-generates for every migration. It's a full schema dump, not new code. The actual hand-written code changes are under 1,000 lines.Migrations applied to production
Before this PR deploys:
0064_add_ai_pricing_catalogapplied manually (tables + indexes, IF NOT EXISTS)0065_add_generations_is_publicapplied manually0067_allow_multiple_oauth_connections_per_userapplied manually (from develop merge)__drizzle_migrationsjournal updated with hashesRemoved from earlier iteration
.github/workflows/approve-pr.yml— Claude review flagged it as critical (shell injection + bypasses branch protection). Removed.PRIVY_MIGRATION_TRIAGE.md— Planning doc shouldn't be in the repo. Removed.Co-authored-by: wakesync [email protected]