Conversation
The OAuth popup-callback flow opens a provider popup, polls window.closed to detect completion or cancellation, and then exchanges the returned code/token. Default Cross-Origin-Opener-Policy of 'same-origin' (or any CORP-isolated default) makes that closed-check throw, so the parent window never notices the popup finishing and the user is stuck in a re-prompt loop. 'same-origin-allow-popups' keeps the page cross-origin-isolated for its own resources but exempts popups it opened, which is the contract the Steward + Privy SDKs are coded against. Scoped to /app-auth/* so dashboard / app pages keep the stricter default. No other change to the existing CSP/security header set.
reworks AppCreditsService.checkBalance / deductCredits / reconcileCredits to read and write organizations.credit_balance via creditsService instead of the per-app app_credit_balances table. signed-in cloud users can now spend their org balance on any monetized app without pre-purchasing a per-app pool. app developers still earn the markup % via the existing recordCreatorEarnings -> redeemableEarnings path. messages route now passes estimatedBaseCost=0 so reconcile charges the full actual cost as the diff (the anonymous reservation never debited upfront, so there is nothing to refund). verified against local cloud: chat call dropped org balance from 1000.000000 to 999.999744 (actual 0.000256), per-app pool unchanged, and a "App reconciliation charge (eDad)" debit row landed in credit_transactions.
…e user menu prior auth flow auto-redirected on isAuthenticated and bailed silently when useAuth().user was null (session loaded but user record not yet hydrated). result: signed-in users saw only a Cancel button with no way to continue. fixed by switching to standard oauth consent ux: - signed out: <StewardLogin> form + Cancel - signed in: "Signed in as <email>" + "Authorize <app>" + Cancel also fetches the email from /api/v1/user when the steward jwt omits it, so the consent label shows the real address instead of "your account". user dropdown in the top-right was overflowing on long emails; added truncate + min-w-0 + title for hover-to-see-full so it stays inside the w-56 menu. minor: dropped a redundant intermediate `result` var in deductCredits — references orgDeduct.newBalance directly.
- inline single-call-site redirectWithError helper into handleCancel - drop "wide" prop from Frame (only ever passed one value) - narrow AppHeader's appInfo to non-null via early-return guard; remove the impossible "?? "A"" / "?? "Application"" / "App logo" defensive fallbacks (AppInfo.name is required by the cloud schema) no behaviour change. types tightened, dead defensive code removed.
…ader
CSP previously only included loopback origins (localhost:3000/3200) when
NODE_ENV=development. Running `next start` against a local Steward
instance with NEXT_PUBLIC_STEWARD_AUTH_ENABLED=true sets NODE_ENV to
production, so the CSP stripped the Steward origin and the browser
refused @stwd/react's fetch to localhost:3200/auth/providers — the
StewardLogin form rendered blank. Widen the gate to also include local
loopback when STEWARD_AUTH_ENABLED is true.
Authorize page: dropped LandingHeader. The header renders different
markup on server vs client based on auth state ("Log in" vs "Dashboard"),
which threw a React hydration error and remounted the whole tree —
that prevented validateApp's effect from completing, leaving the page
stuck on "Verifying application...". Standard OAuth consent UIs
(Google, GitHub) keep this screen header-less for the same reason.
Also dropped a dead <video> tag pointing at an asset not in the repo
(was 404'ing in 60s+ on every page load) and inlined its 1-line
wrapper, replaced with a static gradient.
Re-add the /videos/Hero* asset on the OAuth consent screen layered over the existing gradient. Browsers fire onError on 404, which we use to display:none the <video> element so the gradient underneath stays visible. Net effect: - prod (asset present): plays the hero loop, same as before - local / self-hosted (asset missing): silently falls back to gradient No conditional fetch or env check — the browser's own load behaviour decides at runtime, so the same component works for both deploys.
Removed two non-essential pieces from the consent screen: 1. The email-fetch effect + accountEmail state. We were chasing `user.email` from useAuth, then falling back to a /api/v1/user round-trip when the Steward session didn't surface it. The label now just reads "Signed in" with the green check — same intent, no network dependency, no slow path on cloud cold-start. 2. The <video> hero element. Local dev doesn't ship the asset and the resulting 404 was adding noticeable load time. Solid gradient stays. Net: -55 lines, simpler component, faster page.
|
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 |
Follow-up idea (out of scope for this PR)While testing this in dev, the browser console was noisy with: This happens because Idea: when Why I didn't do it in this PR: safe execution requires per-component verification that each Just flagging in case a maintainer wants to take it on or batch it with another auth-cleanup pass. |
uuid v3/v5/v6 missing buffer bounds check when external buf is provided. v14 adds the same RangeError guard as v1/v4/v7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Made-with: Cursor
TL;DR
Two intertwined cleanups for the app-monetization story plus the OAuth consent flow:
appCreditsServiceso monetized apps debit the user's organization credit balance instead of the per-appapp_credit_balancespool. Any signed-in cloud user can use any monetized app without first topping up a per-app pot. Creator markup share + redeemable_earnings flow still fires throughrecordCreatorEarnings()./app-auth/authorizeUX. Old code auto-redirected onisAuthenticatedand bailed silently whenuseAuth().userhadn't hydrated, which left users staring at a Cancel-only screen. New flow shows an explicit "Authorize " button and a checkmark "Signed in" status. Standard OAuth consent UX, no silent failure modes.Plus the smaller fixes that fell out of testing:
same-origin-allow-popupson/app-auth/*so OAuth popups can callwindow.closedwithout throwing.localhost:3200whenNEXT_PUBLIC_STEWARD_AUTH_ENABLED=true(so a local Steward dev server is reachable from a browser running againstnext start).LandingHeaderfrom the consent screen — its server/client auth-state mismatch ("Log in" vs "Dashboard") was throwing a hydration error that remounted the React tree mid-flight and preventedvalidateApp's effect from completing.<video src="/videos/Hero...mp4">element (asset isn't checked in; was 404'ing in 60s+ on every page load).w-56container.Why this matters
The org-balance rework is the load-bearing piece. Before this PR a user had to discover per-app credits as a separate concept, top up each app individually, and the in-app deduct path went through a different service from the rest of cloud's billing. After this PR, the marketplace promise is honest: one balance, any monetized app, creator + affiliate still earn their cut.
What's verified
End-to-end against a local cloud-fork + Steward + agent-home eDad reference app:
```
ORG balance: 999.999147 → 999.999467 (debited $0.000277 = base + 100% markup)
APP pool: 100.04 → 100.04 (untouched ✅ — no per-app deduction)
CREATOR earnings: 0.000000 → 0.000229 (creator markup share)
PLATFORM revenue: 0.000000 → 0.000229 (base cost)
REDEEMABLE_EARNINGS: 0 → 0.0002 (creator's withdrawable balance)
```
New-user JIT sync also exercised: brand-new Steward JWT →
syncUserFromStewardprovisions cloud user + org with $5 welcome credit → chat charges $0.000588 → creator earnings tick up. Same pattern as existing user.Migration notes for redeploy
app_credit_balancestable is left in place — no data loss, just no longer the deduction target. Per-app pools you've sold remain readable; new chats just don't decrement them.baseCost * (1 + inference_markup_percentage / 100)debited atomically fromorganizations.credit_balanceviacreditsService.reserveAndDeductCredits(same row-lock pattern used by every other org-balance debit in cloud).Code-quality notes
authorize-content.tsx(Frame, AppHeader, PermissionsList, SignedInActions, SignedOutActions) — no new shared abstractions.File-by-file
packages/lib/services/app-credits.ts— checkBalance / deductCredits / reconcileCredits all reach intoorganizations.credit_balanceviacreditsService. Refund path usesrefundCredits.app/api/v1/messages/route.ts— for the app-credits flow,estimatedBaseCostis now0so reconcile charges the full actual cost as the diff (the anonymous reservation never debited upfront).packages/ui/src/components/auth/authorize-content.tsx— full rewrite of the consent screen.packages/ui/src/components/layout/user-menu.tsx—truncate+min-w-0+title=...on the email row.next.config.ts— COOP for/app-auth/*, CSP gate for local Steward.What I want maintainer eyes on
reconcileCredits— I assume the messages-route flow is the only caller that usesestimatedBaseCost: 0. If anything else still passes a real estimate, the "diff" math will under-charge.app_credit_balancestable, drop the dashboard UI for it, dropprocessPurchasefor app credits) in a follow-up. This PR keeps it readable but inert.force-dynamicpage/app-auth/authorizeis heavier in dev-mode webpack compile after this rewrite (it pulls<StewardLogin>from@stwd/react) — if Vercel cold-starts feel slow, considergenerateStaticParamsor splitting StewardLogin to client-only chunk.Test plan
organizations.credit_balancedecrements by base*(1+markup)apps.total_creator_earningsand the creator'sredeemable_earningsrise by base*markupapp_credit_balancesrow for the same app stays untouched