Skip to content

feat(frontend): @agenta/auth + @agenta/auth-ui — one sign-in layer for oss, ee and mobile - #5865

Draft
ardaerzin wants to merge 3 commits into
release/v0.112.0from
pkg/auth
Draft

feat(frontend): @agenta/auth + @agenta/auth-ui — one sign-in layer for oss, ee and mobile#5865
ardaerzin wants to merge 3 commits into
release/v0.112.0from
pkg/auth

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

Sign-in existed three times: OSS had it, EE re-implemented it, and /m had nothing. This lane
makes it one layer.

  • @agenta/auth — the headless half: SuperTokens client wiring, runtime/edition config, SSO
    discovery, the OTP state machine, last-used-method persistence. No React, no antd.
  • @agenta/auth-ui — the rendered half: email-first, email+password, passwordless request, OTP
    verify, social buttons, the divider and the error strip. antd-free, so /m can use it.
  • OSS's auth pages now compose those instead of owning the logic, and /m gets a real
    SignInScreen plus the middleware that guards it.

Unit tests cover the config resolution, SSO discovery, the OTP machine and last-auth-method.

Not run in a browser — static gates only (pnpm lint-fix 24/24, tsc --noEmit clean for
@agenta/shared, ui, entities, entity-ui, settings-ui, oss, ee, mobile).

Bottom of a 29-lane stack on release/v0.112.0; every lane above bases on the one below it.

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
agenta-documentation Error Error Aug 10, 2026 2:57pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 26118f5b-c8bc-43c6-9ea3-f1abf9997e6f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Introduced a unified authentication experience across web and mobile.
    • Added reusable email, password, passwordless, OTP verification, and social sign-in flows.
    • Added OTP resend, cooldown, retry, restart, and clearer error handling.
    • Added support for remembering the last authentication method and displaying provider-specific icons.
    • Added optional security verification support during authentication.
  • Improvements

    • Updated mobile sign-in styling and shared authentication behavior.
    • Added a runtime option to allow desktop access when the mobile reverse gate is disabled.
  • Tests

    • Expanded coverage for authentication configuration, OTP behavior, SSO discovery, and mobile routing.

Walkthrough

Authentication logic and UI are extracted into @agenta/auth and @agenta/auth-ui. OSS and mobile authentication screens now use the shared packages. Mobile middleware adds reverse-gate bypass coverage.

Changes

Authentication runtime and client

Layer / File(s) Summary
Headless authentication runtime and client
web/packages/agenta-auth/...
Adds runtime configuration, SuperTokens operations, structured authentication outcomes, OTP state handling, discovery, last-auth-method helpers, package exports, and unit-test infrastructure and coverage.

Shared authentication UI

Layer / File(s) Summary
Reusable authentication components and styles
web/packages/agenta-auth-ui/...
Adds typed email, password, passwordless, OTP, social-auth, divider, and error components with shared authentication styles and package validation rules.

OSS authentication integration

Layer / File(s) Summary
OSS page integration and security adapter
web/oss/src/components/pages/auth/...
Replaces local authentication forms and rendering with shared components. Adds useTurnstileSecurity and preserves page-level redirects, callbacks, and auth-flow state.

Mobile authentication integration

Layer / File(s) Summary
Mobile sign-in and middleware updates
web/mobile/src/features/auth/..., web/mobile/src/lib/auth/index.ts, web/mobile/src/middleware.ts, web/mobile/tests/unit/middleware.test.ts
Uses shared authentication APIs and UI components, adds provider icon mapping, replaces shared style constants with auth classes, and bypasses the reverse gate when explicitly disabled while preserving mobile opt-in behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the shared authentication layer introduced for OSS, EE, and mobile.
Description check ✅ Passed The description accurately explains the shared auth packages, platform integrations, tests, and validation results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pkg/auth

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (13)
web/oss/src/components/pages/auth/assets/useTurnstileSecurity.tsx (1)

22-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep ref mutations out of render.

Line 25 writes to tokenRef.current in the render body. Mutating refs during render violates the render-purity rule and can become incompatible with compiler-linted React. Move the assignment into a useEffect keyed by token, or avoid token state and store the latest token only in the ref with a forced refresh where the widget needs it.

Proposed change
     const [token, setToken] = useState<string | null>(null)
     const widgetRef = useRef<TurnstileWidgetHandle>(null)
     const tokenRef = useRef<string | null>(null)
-    tokenRef.current = token
+    useEffect(() => {
+        tokenRef.current = token
+    }, [token])

Add useEffect to the React import on line 1.

Source: Coding guidelines

web/mobile/src/features/auth/SignInScreen.tsx (2)

35-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the as AuthMessage cast.

The state type at Line 45 is Partial<AuthMessage>. An empty object already satisfies Partial<AuthMessage>, so the cast is unnecessary. The cast also asserts a false type for any other consumer of EMPTY_MESSAGE.

♻️ Proposed refactor
-const EMPTY_MESSAGE = {} as AuthMessage
+const EMPTY_MESSAGE: Partial<AuthMessage> = {}

107-116: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Memoize the provider array and the callbacks passed to shared components.

Lines 110-113 build a new array of objects on every render. Each object holds a freshly created JSX icon. Lines 83, 91 and 114 create new function identities on every render. SocialAuthButtons, EmailPasswordForm and OtpVerifyForm therefore cannot skip re-renders.

Wrap the provider array in useMemo keyed on methods?.providers, and wrap startProvider and the onSuccess adapters in useCallback.

As per coding guidelines: "Memoize inline arrays containing objects or JSX when passing them as props to avoid unnecessary rerenders" and "Minimize React re-renders with useMemo, useCallback, and React.memo where appropriate; avoid unstable inline functions and objects, especially in lists."

Source: Coding guidelines

web/mobile/src/features/auth/providerIcons.tsx (1)

5-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Note the follow-up and the google gap.

lucide-react has no Google brand glyph, so google falls to the Globe default. google is the first provider in OIDC_PROVIDER_META, so the most common button on the screen shows a generic globe. The comment on Lines 5-9 already records the shared inline-SVG plan.

Do you want me to open an issue to track the shared brand-icon set in @agenta/auth-ui?

The coding guidelines ask for at most one short comment line, and longer comments only for surprising constraints such as bugs, races, or ordering requirements. Consider moving the four-line follow-up note to the tracking issue and keeping one line here.

Source: Coding guidelines

web/mobile/src/features/auth/SsoDiscoveryForm.tsx (1)

51-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider exporting the auth class names as constants from @agenta/auth-ui.

The change replaces authQuietButtonClass, authPrimaryButtonClass and the input style constant with literal strings. The strings now appear in this file and in SignInScreen.tsx. A typo in auth-surface-btn no longer fails the build, and a rename in auth.css cannot be traced by the compiler.

Exporting the class names from @agenta/auth-ui next to auth.css restores a single source of truth and keeps the styling in the shared stylesheet.

Note also that Line 66 changes the provider buttons from the former primary style to auth-surface-btn, while Line 101 gives the submit button the same auth-surface-btn. Confirm that the loss of a distinct primary style is intended.

Also applies to: 66-66, 78-78, 94-101

web/mobile/tests/unit/middleware.test.ts (1)

44-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a non-"false" flag value.

The two new tests cover the disabled path only. The middleware compares the flag against the exact string "false". Add a case that sets AGENTA_MOBILE_REVERSE_GATE = "0" and asserts a desktop UA still redirects. That test pins the current strict-equality behavior, and it will fail if the comparison changes to a normalized check.

Consider also asserting the redirect status, not only the absence of a location header. expect(res.status).toBe(200) distinguishes a pass-through from a redirect that lacks a location header.

💚 Proposed test
     it("?view=mobile still sets the opt-in cookie with the reverse gate off", () => {
         process.env.AGENTA_MOBILE_REVERSE_GATE = "false"
         const res = middleware(req("/m/?view=mobile", doc(DESKTOP_UA)))
         expect(res.headers.get("set-cookie") ?? "").toContain("agenta-mobile-optin=1")
     })
+
+    it("redirects desktop UAs for any reverse-gate value other than \"false\"", () => {
+        process.env.AGENTA_MOBILE_REVERSE_GATE = "0"
+        const res = middleware(req("/m/", doc(DESKTOP_UA)))
+        expect(res.status).toBe(307)
+        expect(res.headers.get("location")).not.toBeNull()
+    })
web/packages/agenta-auth-ui/src/types.ts (1)

10-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce multiline implementation comments.

Use one short comment only when the code needs context. The current comments describe behavior that names and types can express.

  • web/packages/agenta-auth-ui/src/types.ts#L10-L14: replace the multiline interface comment with one short constraint comment, or remove it.
  • web/packages/agenta-auth-ui/src/EmailPasswordForm.tsx#L21-L24: replace the multiline component comment with one short comment, or remove it.

As per coding guidelines, “Keep in-code comments to at most one short line.”

Source: Coding guidelines

web/packages/agenta-auth/src/discovery.ts (1)

11-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider hosting parseWithLogging in @agenta/shared.

The guidelines require safeParseWithLogging from @agenta/entities/shared at API boundaries. The layering note is valid, because @agenta/auth must not depend on @agenta/entities. @agenta/shared sits below both packages and can hold this helper. A shared home removes the duplicate implementation and keeps one logging format.

As per coding guidelines: "Keep Zod validation at API boundaries using safeParseWithLogging from @agenta/entities/shared" and "respect the hierarchy shared ← ui ← entities ← entity-ui ← playground ← playground-ui".

Source: Coding guidelines

web/packages/agenta-auth/src/lastAuthMethod.ts (1)

6-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider namespacing the storage key and returning the trimmed value.

The guidelines require the agenta: prefix on persisted storage keys. This key is the bare string "lastAuthMethod". If you rename it, read the old key as a fallback first, because existing OSS users would otherwise lose the "Welcome back" state. Line 14 also tests value.trim() but returns the untrimmed value, so a padded entry never matches a provider id.

As per coding guidelines: "prefix storage keys with agenta:".

Source: Coding guidelines

web/packages/agenta-auth-ui/src/OtpInput.tsx (1)

66-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Set maxLength to 1 and label each cell.

Each cell holds one character, but maxLength={length} allows six characters in a single cell. onFocus selection hides this in most cases; a caret placed without selection still lets handleChange insert several characters and shift the value. Paste is not affected, because handlePaste calls preventDefault.

Each cell also has no accessible name. Screen readers announce an unlabeled text field six times.

♻️ Proposed change
                     className="auth-otp-cell"
                     inputMode="text"
+                    aria-label={`Digit ${index + 1} of ${length}`}
                     autoComplete={index === 0 ? "one-time-code" : "off"}
                     autoFocus={autoFocus && index === 0}
-                    maxLength={length}
+                    maxLength={1}
web/packages/agenta-auth-ui/src/OtpVerifyForm.tsx (1)

99-108: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Handle rejections and re-entry in resend.

submit wraps its network call in try/catch, but resend does not. If resendEmailCode rejects, the click handler produces an unhandled rejection and the user sees no message. The button also stays enabled during the call, so a fast double click sends two codes.

♻️ Proposed change
     const resend = async () => {
-        const outcome = await resendEmailCode()
-        if (outcome.kind === "ok") {
-            setMessage({message: "New code sent successfully", type: "info"})
-            setResendBlocked(true)
-        } else {
-            setMessage({message: "Resend OTP failed. Please try again", type: "error"})
-            await restart()
-        }
+        if (isLoading) return
+        try {
+            setIsLoading(true)
+            const outcome = await resendEmailCode()
+            if (outcome.kind === "ok") {
+                setMessage({message: "New code sent successfully", type: "info"})
+                setResendBlocked(true)
+            } else {
+                setMessage({message: "Resend OTP failed. Please try again", type: "error"})
+                await restart()
+            }
+        } catch (error) {
+            onAuthError?.(error)
+            setMessage({message: "Resend OTP failed. Please try again", type: "error"})
+        } finally {
+            setIsLoading(false)
+        }
     }
web/packages/agenta-auth-ui/src/ShowErrorMessage.tsx (1)

12-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Announce the error to assistive technology.

The parent components mount this element only when an error appears. Without a live region, screen readers do not announce the sign-in failure. Add role="alert".

-    <div className={clsx("mb-4 text-center", className)}>
+    <div role="alert" className={clsx("mb-4 text-center", className)}>
web/packages/agenta-auth-ui/src/auth.css (1)

44-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Raw hex values conflict with the theme-token guideline.

The coding guidelines require theme colors through Ant Design semantic tokens, Tailwind color utilities, or supported var(--ag-color*) variables, and forbid raw hex. This block defines a parallel --a-* palette in hex. The file header explains the intent, and the tokens are scoped under .auth-redesign, so the styles do not leak. Confirm that the design owners accept this exception, or map the --a-* tokens onto var(--ag-color*) values where an equivalent exists.

As per coding guidelines: "Consume theme colors through Ant Design semantic tokens, Tailwind color utilities, or supported var(--ag-color*) variables; do not use raw hex colors or --ag-c-* literals."

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 72bf45f8-6928-4e05-92d6-c0453bca367b

📥 Commits

Reviewing files that changed from the base of the PR and between 613368b and 5ee8332.

📒 Files selected for processing (49)
  • web/mobile/src/features/auth/AuthDivider.tsx
  • web/mobile/src/features/auth/EmailOtpForm.tsx
  • web/mobile/src/features/auth/EmailPasswordForm.tsx
  • web/mobile/src/features/auth/OidcProviderButtons.tsx
  • web/mobile/src/features/auth/SignInScreen.tsx
  • web/mobile/src/features/auth/SsoDiscoveryForm.tsx
  • web/mobile/src/features/auth/authStyles.ts
  • web/mobile/src/features/auth/providerIcons.tsx
  • web/mobile/src/lib/auth/index.ts
  • web/mobile/src/middleware.ts
  • web/mobile/tests/unit/middleware.test.ts
  • web/oss/src/components/pages/auth/EmailFirst/index.tsx
  • web/oss/src/components/pages/auth/EmailPasswordAuth/index.tsx
  • web/oss/src/components/pages/auth/EmailPasswordSignIn/index.tsx
  • web/oss/src/components/pages/auth/PasswordlessAuth/index.tsx
  • web/oss/src/components/pages/auth/SendOTP/index.tsx
  • web/oss/src/components/pages/auth/SocialAuth/index.tsx
  • web/oss/src/components/pages/auth/assets/ShowErrorMessage.tsx
  • web/oss/src/components/pages/auth/assets/lastAuthMethod.ts
  • web/oss/src/components/pages/auth/assets/useTurnstileSecurity.tsx
  • web/packages/agenta-auth-ui/eslint.config.mjs
  • web/packages/agenta-auth-ui/package.json
  • web/packages/agenta-auth-ui/src/AuthDivider.tsx
  • web/packages/agenta-auth-ui/src/EmailFirstForm.tsx
  • web/packages/agenta-auth-ui/src/EmailPasswordForm.tsx
  • web/packages/agenta-auth-ui/src/OtpInput.tsx
  • web/packages/agenta-auth-ui/src/OtpVerifyForm.tsx
  • web/packages/agenta-auth-ui/src/PasswordlessRequestForm.tsx
  • web/packages/agenta-auth-ui/src/ShowErrorMessage.tsx
  • web/packages/agenta-auth-ui/src/SocialAuthButtons.tsx
  • web/packages/agenta-auth-ui/src/auth.css
  • web/packages/agenta-auth-ui/src/index.ts
  • web/packages/agenta-auth-ui/src/types.ts
  • web/packages/agenta-auth-ui/tsconfig.json
  • web/packages/agenta-auth/eslint.config.mjs
  • web/packages/agenta-auth/package.json
  • web/packages/agenta-auth/src/client.ts
  • web/packages/agenta-auth/src/config.ts
  • web/packages/agenta-auth/src/discovery.ts
  • web/packages/agenta-auth/src/index.ts
  • web/packages/agenta-auth/src/lastAuthMethod.ts
  • web/packages/agenta-auth/src/otpMachine.ts
  • web/packages/agenta-auth/src/runtime.ts
  • web/packages/agenta-auth/tests/unit/authConfig.test.ts
  • web/packages/agenta-auth/tests/unit/authDiscover.test.ts
  • web/packages/agenta-auth/tests/unit/lastAuthMethod.test.ts
  • web/packages/agenta-auth/tests/unit/otpMachine.test.ts
  • web/packages/agenta-auth/tsconfig.json
  • web/packages/agenta-auth/vitest.config.ts
💤 Files with no reviewable changes (5)
  • web/mobile/src/features/auth/AuthDivider.tsx
  • web/mobile/src/features/auth/EmailPasswordForm.tsx
  • web/mobile/src/features/auth/EmailOtpForm.tsx
  • web/mobile/src/features/auth/OidcProviderButtons.tsx
  • web/mobile/src/features/auth/authStyles.ts

Comment on lines +59 to +66
const startProvider = async (providerId: string) => {
if (oidcLoading) return
setOidcLoading(true)
// Resolves only on failure — success navigates away.
await startOidcSignIn(providerId)
setOidcLoading(false)
setMessage({message: "Could not reach that provider. Try again.", type: "error"})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add error handling and reset oidcLoading in a finally block.

startProvider calls startOidcSignIn without a try/catch. The call site at Line 114 uses void startProvider(providerId), so it discards the returned promise. If startOidcSignIn rejects, two failures follow:

  1. setOidcLoading(false) never runs. oidcLoading stays true, and SocialAuthButtons stays disabled until the user reloads the page.
  2. The rejection becomes an unhandled promise rejection.

SsoDiscoveryForm.start at Lines 39-47 has the same shape, so the fix should apply to both call sites.

🛡️ Proposed fix
     const startProvider = async (providerId: string) => {
         if (oidcLoading) return
         setOidcLoading(true)
-        // Resolves only on failure — success navigates away.
-        await startOidcSignIn(providerId)
-        setOidcLoading(false)
-        setMessage({message: "Could not reach that provider. Try again.", type: "error"})
+        try {
+            // Resolves only on failure — success navigates away.
+            await startOidcSignIn(providerId)
+        } finally {
+            setOidcLoading(false)
+            setMessage({message: "Could not reach that provider. Try again.", type: "error"})
+        }
     }

Comment on lines +125 to +128
<div className="auth-redesign flex min-h-dvh flex-col items-center justify-center gap-8 p-6">
<header className="flex flex-col items-center gap-3">
<AgentaLogo className="text-foreground h-6 w-auto" />
<p className="text-muted-foreground text-xs">Sign in or create an account.</p>
<AgentaLogo className="h-6 w-auto text-[var(--a-heading)]" />
<p className="auth-subline m-0">Sign in or create an account.</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the auth design tokens and the stylesheet import chain for the mobile app.
set -uo pipefail

echo "=== auth.css token + class definitions ==="
fd -t f 'auth.css' | while IFS= read -r f; do
  echo "--- $f"
  rg -n -- '--a-heading|auth-subline|auth-redesign|auth-quiet-btn|auth-surface-btn|auth-input|prefers-color-scheme|\.dark' "$f"
done

echo "=== who imports auth.css ==="
rg -n --iglob '*.{ts,tsx,css,json}' 'auth-ui/.*auth\.css|auth\.css'

echo "=== agenta-auth-ui package exports ==="
fd -t f 'package.json' -p 'agenta-auth-ui' --exec cat {}

echo "=== mobile global stylesheet imports ==="
fd -t f -e tsx -e ts -e css . web/mobile/src --exec rg -ln 'auth\.css|globals\.css' {} \;

Repository: Agenta-AI/agenta

Length of output: 2677


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "=== web/packages/agenta-auth-ui package files ==="
fd -t f . web/packages/agenta-auth-ui -x sh -c 'echo "--- $1"; case "$1" in package.json|src/*) cat "$1" ;; esac' sh {}

echo "=== auth.css token/class declarations (focused) ==="
python3 - <<'PY'
from pathlib import Path
for p in map(Path, ['web/packages/agenta-auth-ui/src/auth.css','web/oss/src/styles/auth.css']):
    print(f'--- {p}')
    text=p.read_text()
    for needle in ['--a-heading', '.auth-subline', '.auth-redesign']:
        idx=text.find(needle)
        print(f'{needle}: {idx}')
        if idx!=-1:
            start=max(0,text.rfind('\n',0,idx-200)-sum(4*40))
            end=min(len(text),text.find('\n', idx+300))
            print(text[start:end])
PY

echo "=== mobile top-level imports ==="
fd -t f -e tsx -e ts -e css web/mobile/src | xargs -r sed -n '1,80p'

echo "=== global/css imports references ==="
rg -n --iglob '*.{css,html,json,ts,tsx}' 'globals\.css|auth\.css|agenta-auth-ui|antd/dist|ag-color' web/mobile web/packages/agenta-auth-ui web/oss || true

Repository: Agenta-AI/agenta

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "=== auth package exports/index.ts ==="
cat -n web/packages/agenta-auth-ui/package.json
echo "---"
cat -n web/packages/agenta-auth-ui/src/index.ts

echo "=== mobile stylesheets ==="
for f in $(fd -t f -e ts -e tsx web/mobile/src | head -50); do
  if rg -n 'globals\.css|auth\.css|agenta-auth-ui' "$f" >/dev/null 2>&1; then
    echo "--- $f"
    sed -n '1,80p' "$f"
  fi
done

echo "=== mobile globals.css ==="
cat -n web/mobile/src/styles/globals.css

echo "=== focused css imports in mobile/package files ==="
rg -n --iglob '*.{ts,tsx,json}' '{import[^(]*"\`@agenta/auth-ui/auth`\.css"|import[^(]*\`@agenta/auth-ui/auth`|auth\.css}' web/mobile web/packages/agenta-auth-ui || true

echo "=== TypeScript references/import paths for ag/agenta packages ==="
for f in web/tsconfig.json web/mobile/tsconfig*.json package.json web/package.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n '`@agenta/auth-ui`|paths|references' "$f" || true
  fi
done

Repository: Agenta-AI/agenta

Length of output: 5632


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "=== focused auth.css definition ==="
python3 - <<'PY'
from pathlib import Path
for p in ['web/packages/agenta-auth-ui/src/auth.css','web/oss/src/styles/auth.css','web/mobile/src/styles/globals.css']:
    path=Path(p)
    if not path.exists():
        print(f'--- {p}: missing')
        continue
    text=path.read_text()
    print(f'--- {p}')
    for needle in ['--a-heading', '.auth-subline', '.auth-redesign']:
        idx=text.find(needle)
        print(f'{needle}: {idx}')
    # print concise block around relevant declarations for mobile globals
    if p == 'web/mobile/src/styles/globals.css':
        lines=text.splitlines()
        for i,line in enumerate(lines,1):
            if any(x in line for x in ['--a-heading','auth-subline','.auth-redesign','theme.generated','palette']):
                print(f'{i}: {line}')
PY

echo "=== mobile _app.tsx ==="
sed -n '1,120p' web/mobile/src/pages/_app.tsx

echo "=== focused auth css references in mobile ==="
rg -n --iglob '*.{ts,tsx,css}' '`@agenta/auth-ui/auth`\.css|auth\.css|auth-redesign|auth-subline|--a-heading' web/mobile web/packages/agenta-auth-ui web/oss/src/styles || true

echo "=== package exports presence ==="
python3 - <<'PY'
import json
from pathlib import Path
p=json.loads(Path('web/package.json').read_text())
for k,v in p.get('packageManager',{}).items():
    if k=='pnpm':
        print('packageManager:',v)
for f in ['web/packages/agenta-auth-ui/package.json','web/mobile/package.json','web/package.json']:
    print('---', f)
    pkg=json.loads(Path(f).read_text())
    print({name:p.get('sideEffects',True) for name,p in pkg.get('imports',{}).items()})
    print('exports.auth-css' in str(pkg.get('exports')))
PY

Repository: Agenta-AI/agenta

Length of output: 6313


Import the auth design stylesheet in the mobile Shell app.

web/packages/agenta-auth-ui/src/auth.css provides --a-heading, .auth-redesign, and .auth-subline with light and dark values, but mobile only imports src/styles/theme.generated.css; it does not import @agenta/auth-ui/auth.css or the equivalent auth CSS. Add the package entry in _app.tsx/global styles so these classes are resolved and the mobile sign-in screen does not render unstyled.

Source: Coding guidelines

Comment on lines +1 to +3
import {EmailFirstForm} from "@agenta/auth-ui"

import {Form, Input} from "antd"
import clsx from "clsx"

import ShowErrorMessage from "../assets/ShowErrorMessage"

interface EmailFirstProps {
email: string
setEmail: (email: string) => void
onContinue: (email: string) => Promise<void>
message: {message: string; sub?: string; type?: "error" | "success" | "info" | "warning"}
disabled?: boolean
// Yellow keycap Continue (the primary action) vs a neutral surface button.
primary?: boolean
// Returning last-used slot: taller input with an inline "Last used" tag.
promoted?: boolean
}

const EmailFirst = ({
email,
setEmail,
onContinue,
message,
disabled,
primary = true,
promoted = false,
}: EmailFirstProps) => {
const [isLoading, setIsLoading] = useState(false)

const handleSubmit = async (values: {email: string}) => {
try {
setIsLoading(true)
await onContinue(values.email)
} finally {
setIsLoading(false)
}
}

return (
<Form
className="w-full flex flex-col gap-[10px]"
layout="vertical"
onFinish={handleSubmit}
initialValues={{email}}
>
<div className="relative">
<Form.Item
name="email"
className="[&_.ant-form-item-required]:before:!hidden w-full mb-0 flex flex-col gap-1"
rules={[
{required: true, message: "Please add your email!"},
{type: "email", message: "Please enter a valid email address!"},
]}
>
<Input
type="email"
placeholder="Enter your email address"
status={message.type === "error" ? "error" : ""}
onChange={(e) => setEmail(e.target.value)}
disabled={disabled}
className={clsx("auth-input", promoted && "auth-input-promoted")}
/>
</Form.Item>
{promoted && (
<span className="auth-last-used-tag absolute right-3 top-1/2 -translate-y-1/2">
Last used
</span>
)}
</div>

<button
type="submit"
className={clsx(primary ? "auth-btn-yellow" : "auth-surface-btn")}
disabled={disabled || isLoading}
>
Continue
</button>
{message.type === "error" && <ShowErrorMessage info={message} className="text-start" />}
</Form>
)
}

export default EmailFirst
export default EmailFirstForm

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify workspace membership, manifest declarations, and package export barrels.
set -euo pipefail

echo "==== workspace config ===="
fd -t f 'pnpm-workspace.yaml' web --max-depth 2 --exec cat {}

echo "==== OSS manifest deps ===="
fd -t f 'package.json' web/oss --max-depth 1 --exec jq '{name, dependencies, devDependencies}' {}

echo "==== new package manifests ===="
fd -t f 'package.json' web/packages/agenta-auth web/packages/agenta-auth-ui --max-depth 2 \
  --exec sh -c 'echo "-- $1"; jq "{name, version, main, module, types, exports, dependencies, peerDependencies}" "$1"' _ {}

echo "==== exported symbols used by OSS ===="
rg -n 'export' web/packages/agenta-auth-ui/src/index.ts web/packages/agenta-auth/src/index.ts

Repository: Agenta-AI/agenta

Length of output: 8187


Declare the new auth workspace packages in web/oss/package.json. web/oss imports @agenta/auth-ui and re-exports @agenta/auth, but the OSS manifest does not declare either package. Add both with the workspace protocol: "@agenta/auth": "workspace:../packages/agenta-auth" and "@agenta/auth-ui": "workspace:../packages/agenta-auth-ui".

📍 Affects 3 files
  • web/oss/src/components/pages/auth/EmailFirst/index.tsx#L1-L3 (this comment)
  • web/oss/src/components/pages/auth/assets/ShowErrorMessage.tsx#L1-L3
  • web/oss/src/components/pages/auth/assets/lastAuthMethod.ts#L1-L1

Comment on lines 7 to +30
const PasswordlessAuth = ({
email,
setEmail,
isLoading,
message,
setIsLoading,
setMessage,
authErrorMsg,
setIsLoginCodeVisible,
disabled,
lockEmail = false,
}: PasswordlessAuthProps) => {
const [turnstileToken, setTurnstileToken] = useState<string | null>(null)
const turnstileEnabled = isTurnstileEnabled()
const turnstileRef = useRef<TurnstileWidgetHandle>(null)

const resetTurnstile = () => {
clearPendingTurnstileToken()
setTurnstileToken(null)
turnstileRef.current?.reset()
}

const ensureTurnstileToken = () => {
if (!turnstileEnabled || turnstileToken) {
return true
}

setMessage({
message: "Please complete the security check.",
type: "error",
})

return false
}

const sendOTP: FormProps<{email: string}>["onFinish"] = async (values) => {
if (!ensureTurnstileToken()) {
return
}

try {
setIsLoading(true)
if (turnstileEnabled) {
setPendingTurnstileToken(turnstileToken)
}
const response = await createCode({email: values.email})

if (response.status === "SIGN_IN_UP_NOT_ALLOWED") {
setMessage({message: response.reason, type: "error"}) // the reason string is a user friendly message
} else {
setMessage({
message: "Check your inbox for the OTP to continue!",
type: "success",
})
setIsLoginCodeVisible(true)
}
} catch (err) {
authErrorMsg(err)
} finally {
resetTurnstile()
setIsLoading(false)
}
}
const security = useTurnstileSecurity(setMessage)

return (
<Form className="w-full space-y-2" onFinish={sendOTP} initialValues={{email}}>
{message.type == "error" && <ShowErrorMessage info={message} />}

<Form.Item
name="email"
className="w-full mb-0"
rules={[{required: true, message: "Please input your email!"}]}
>
<Input
size="large"
type="email"
value={email}
placeholder="Enter valid email address"
disabled={lockEmail}
className={lockEmail ? "auth-locked-input" : undefined}
onChange={(e) => setEmail(e.target.value)}
/>
</Form.Item>

{turnstileEnabled && (
<TurnstileWidget
ref={turnstileRef}
className="flex justify-center"
onTokenChange={setTurnstileToken}
onError={() =>
setMessage({
message: "Security check failed. Please try again.",
type: "error",
})
}
/>
)}

<Button
size="large"
type="primary"
htmlType="submit"
className="w-full"
loading={isLoading}
disabled={disabled}
>
Continue with OTP
</Button>
</Form>
<PasswordlessRequestForm
email={email}
setEmail={setEmail}
message={message}
setMessage={setMessage}
onCodeSent={() => setIsLoginCodeVisible(true)}
onAuthError={authErrorMsg}
disabled={disabled}
lockEmail={lockEmail}
security={security}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Trace how the parent page uses isLoading for the passwordless flow.
set -euo pipefail

rg -n -C6 'PasswordlessAuth' web/oss/src --type=tsx --type=ts
echo "---- shared form props ----"
fd -t f 'PasswordlessRequestForm.tsx' web/packages/agenta-auth-ui --exec cat -n {}
echo "---- loading-related props in shared types ----"
rg -n -C3 -i 'loading' web/packages/agenta-auth-ui/src/types.ts

Repository: Agenta-AI/agenta

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)PasswordlessAuth/index\.tsx$|(^|/)assets/types\.d\.ts$|(^|/)PasswordlessRequestForm\.tsx$|(^|/)types\.ts$'

echo "---- PasswordlessAuth usages/definition ----"
rg -n -C5 'PasswordlessAuth' web/oss/src

echo "---- PasswordlessRequestForm definitions ----"
fd -t f 'PasswordlessRequestForm\.tsx$' . --exec sh -c 'echo "--- $1"; cat -n "$1"' sh {}

echo "---- loading-related props/types in auth files ----"
rg -n -C3 -i 'loading|isLoading|setIsLoading' web/oss/src web/packages/agenta-auth-ui/src --glob '*.{ts,tsx}'

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== targeted files =="
git ls-files | rg 'PasswordlessAuth/index\.tsx$|PasswordlessRequestForm\.tsx$|auth/assets/types\.d\.ts$'

echo "---- PasswordlessAuth parent components near PasswordlessAuth usage ----"
python3 - <<'PY'
import subprocess, pathlib
files=subprocess.check_output(['git','ls-files'],text=True).splitlines()
for f in files:
    if f.endswith(('.tsx','.ts')) and f.startswith('web/oss/src/components/pages/auth/'):
        try:
            text=pathlib.Path(f).read_text()
        except UnicodeDecodeError:
            continue
        if 'PasswordlessAuth' in text or 'SocialAuth' in text or 'isLoading' in text:
            print(f'--- {f}')
            lines=text.splitlines()
            for i,l in enumerate(lines,1):
                if 'PasswordlessAuth' in l or 'SocialAuth' in l or 'isLoading' in l:
                    a=max(1,i-8); b=min(len(lines),i+8)
                    print(f'@@ {a}-{b}')
                    for j in range(a,b+1):
                        print(f'{j:4}: {lines[j-1]}')
                    print()
PY

echo "---- exact relevant source ----"
for f in $(git ls-files | rg 'PasswordlessAuth/index\.tsx$|(^|/)PasswordlessRequestForm\.tsx$|auth/assets/types\.d\.ts$'); do
  echo "--- $f"
  wc -l "$f"
  cat -n "$f"
done

Repository: Agenta-AI/agenta

Length of output: 16909


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== auth page files =="
git ls-files 'web/oss/src/components/pages/auth/**'

echo "---- auth page usages of PasswordlessAuth/SocialAuth/isLoading ----"
rg -n -C8 'PasswordlessAuth|SocialAuth|isLoading|setIsLoading' web/oss/src/components/pages/auth --glob '*.{ts,tsx}'

echo "---- exact parent auth page files (limited) ----"
for f in $(git ls-files 'web/oss/src/components/pages/auth/**'); do
  if [ -f "$f" ]; then
    lines=$(wc -l < "$f")
    if [ "$lines" -le 180 ]; then
      echo "--- $f ($lines lines)"
      cat -n "$f"
    fi
  fi
done

Repository: Agenta-AI/agenta

Length of output: 37177


Align the passwordless loading contract with the shared form.

PasswordlessRequestForm manages its own request loading and can trigger onCodeSent; it has no isLoading/setIsLoading props. The current type contract for PasswordlessAuthProps declares these props that PasswordlessAuth cannot pass through. Either move the loading state from the parent into the shared form, expose a loading callback instead, or remove isLoading/setIsLoading from this contract and any parent consumers.

Comment on lines +28 to +33
onSuccess={async (payload) => {
await handleAuthSuccess(
{createdNewRecipeUser: true, user},
{createdNewRecipeUser: true, user: payload.user},
{isInvitedUser, authMethod: "email"},
)
} else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") {
const trileLeft =
response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount
setMessage({
message: "Invalid code, Please try again.",
sub: `Retry available ${trileLeft}`,
type: "error",
})
} else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") {
setMessage({
message: "Your code has expried",
sub: "Please request for a new code below",
type: "error",
})
} else {
setMessage({
message: "Authentication failed. Please try again",
type: "error",
})
await clearLoginAttemptInfo()
setIsLoginCodeVisible(false)
setAuthFlow("unauthed")
}
} catch (err) {
authErrorMsg(err)
setAuthFlow("unauthed")
} finally {
setIsLoading(false)
}
}

const backToLogin = async () => {
await clearLoginAttemptInfo()
setIsLoginCodeVisible(false)
}

return (
<div className="w-full">
<Form
autoComplete="off"
onFinish={submitOTP}
className="w-full flex flex-col gap-4"
initialValues={{email}}
>
{message.type == "error" && <ShowErrorMessage info={message} />}

<Form.Item name="email" className="w-full mb-0 flex flex-col gap-1">
<Input
size="large"
type="email"
value={email}
placeholder="Enter valid email address"
disabled
className="auth-locked-input"
/>
</Form.Item>

<Form.Item
name="otp"
className={clsx(
message.type == "error" &&
"[&_.ant-otp_.ant-input]:border [&_.ant-otp_.ant-input]:border-solid [&_.ant-otp_.ant-input]:border-colorErrorBorder",
"w-full mb-0 [&_.ant-otp]:w-full [&_.ant-otp_.ant-otp-input-wrapper]:w-full",
)}
rules={[
{
required: true,
message: "Invalid OTP!",
min: 6,
},
]}
>
<Input.OTP
formatter={(str) => str.toUpperCase()}
autoFocus={true}
ref={inputRef}
/>
</Form.Item>

<Button
size="large"
type="primary"
htmlType="submit"
className="w-full"
loading={isLoading}
>
Continue with OTP
</Button>
</Form>

<div className="grid gap-2 text-center mt-4">
<Button
type="link"
className="w-full"
icon={<ArrowLeft size={14} className="mt-[3px]" />}
onClick={backToLogin}
>
Use a different email
</Button>
<Button
type="link"
className="w-full"
disabled={isResendDisabled || isLoading}
onClick={resendOTP}
>
Resend one-time password
</Button>
{isResendDisabled && (
<Text className="text-colorTextDisabled">
Please wait to request new code (60s)
</Text>
)}
</div>
</div>
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the OtpVerifyForm success payload shape and how handleAuthSuccess uses createdNewRecipeUser.
set -euo pipefail

fd -t f 'OtpVerifyForm.tsx' web/packages/agenta-auth-ui --exec cat -n {}
echo "---- payload/success types ----"
rg -n -C4 'createdNewRecipeUser' web/packages web/oss/src
echo "---- redirect hook ----"
fd -t f 'usePostAuthRedirect*' web/oss/src --exec cat -n {}

Repository: Agenta-AI/agenta

Length of output: 34877


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- SendOTP usage and handleAuthSuccess call sites ----"
rg -n -C5 'handleAuthSuccess\(|createdNewRecipeUser: true|createdNewRecipeUser' web/oss/src web/packages/agenta-auth web/packages/agenta-auth-ui | sed -n '1,220p'

echo "---- OTP consume helper implementation ----"
sed -n '270,302p' web/packages/agenta-auth/src/client.ts

Repository: Agenta-AI/agenta

Length of output: 15903


🌐 Web query:

SuperTokens Passwordless consumeCode response createdNewRecipeUser is_new_user

💡 Result:

In the SuperTokens Passwordless recipe, the consumeCode function (or API endpoint) returns a boolean property named createdNewRecipeUser (or createdNewUser in some core API contexts) [1][2][3]. This property indicates whether a new user record was created in the system as a result of the successful authentication [3]. When a user signs in using a passwordless method (email or phone number): - If they do not already exist in the user store, SuperTokens creates a new user and sets createdNewRecipeUser to true [3]. - If they already exist, SuperTokens retrieves the existing user and sets createdNewRecipeUser to false [3]. In the context of the backend SDKs and standard API responses, you can use this flag to determine if you need to perform any "first-time user" logic, such as sending a welcome email, initializing user profiles, or tracking new sign-ups [3]. While the core API documentation may sometimes refer to the field as createdNewUser, the language-specific SDKs (like Node.js) consistently use the property name createdNewRecipeUser [1][2][4][5].

Citations:


🌐 Web query:

SuperTokens frontend SDK JavaScript Passwordless consumeCode createdNewRecipeUser

💡 Result:

In the SuperTokens frontend JavaScript SDK (such as supertokens-web-js or supertokens-auth-react), the consumeCode function is used to verify a passwordless authentication code and complete the sign-in or sign-up process [1][2][3]. When this function is called and the operation is successful (resulting in a status of "OK"), the returned object includes a createdNewRecipeUser property [1][4][5]. Key details about createdNewRecipeUser: - Type: boolean [1][2][5]. - Meaning: It indicates whether a new user was created in the database during this authentication attempt [6]. If true, it means the email or phone number provided had not been registered previously, and a new account was created [6]. If false, it means the authentication was for an existing user [6]. Example usage: const response = await supertokensPasswordless.consumeCode({ userInputCode: "123456" }); if (response.status === "OK") { console.log("User signed in successfully."); console.log("Was a new user created?", response.createdNewRecipeUser); console.log("User details:", response.user); } The consumeCode function also returns other properties upon success, such as the user object and the fetchResponse [1][4][5]. If the code input is incorrect, expired, or if the sign-in/up process is not allowed, the function returns a different status (e.g., INCORRECT_USER_INPUT_CODE_ERROR, EXPIRED_USER_INPUT_CODE_ERROR, or SIGN_IN_UP_NOT_ALLOWED) [1][4][5].

Citations:


Do not replace the OTP response with createdNewRecipeUser: true.

submitEmailCodeDetailed hard-codes this value on success, and handleAuthSuccess uses authResult.createdNewRecipeUser as the EE fallback for new-user redirects. If Passwordless.consumeCode returned false for an existing user, the response is lost here and an EE returner can take the new-user path. Keep or forward the actual OTP success flag from the wrapper; if the wrapper currently never changes it, update the wrapper to return outcome.createdNewRecipeUser.

Comment on lines +46 to +55
const handleKeyDown = (index: number, event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Backspace") {
event.preventDefault()
const cut = index >= value.length ? value.length - 1 : index
setChars(value.slice(0, Math.max(0, cut)))
focusCell(cut)
}
if (event.key === "ArrowLeft") focusCell(index - 1)
if (event.key === "ArrowRight") focusCell(index + 1)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Backspace in a middle cell deletes all following characters.

setChars(value.slice(0, Math.max(0, cut))) truncates the value at cut. If the value is ABC123 and the user presses Backspace in cell 2, the value becomes A. The characters after the caret are lost, although the user only intended to delete one character.

Remove one character and keep the remainder.

🐛 Proposed fix
     const handleKeyDown = (index: number, event: KeyboardEvent<HTMLInputElement>) => {
         if (event.key === "Backspace") {
             event.preventDefault()
-            const cut = index >= value.length ? value.length - 1 : index
-            setChars(value.slice(0, Math.max(0, cut)))
-            focusCell(cut)
+            const target = value[index] ? index : index - 1
+            if (target < 0) return
+            setChars(value.slice(0, target) + value.slice(target + 1))
+            focusCell(target)
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleKeyDown = (index: number, event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Backspace") {
event.preventDefault()
const cut = index >= value.length ? value.length - 1 : index
setChars(value.slice(0, Math.max(0, cut)))
focusCell(cut)
}
if (event.key === "ArrowLeft") focusCell(index - 1)
if (event.key === "ArrowRight") focusCell(index + 1)
}
const handleKeyDown = (index: number, event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Backspace") {
event.preventDefault()
const target = value[index] ? index : index - 1
if (target < 0) return
setChars(value.slice(0, target) + value.slice(target + 1))
focusCell(target)
}
if (event.key === "ArrowLeft") focusCell(index - 1)
if (event.key === "ArrowRight") focusCell(index + 1)
}

Comment on lines +70 to +73
if (outcome.kind === "ok") {
await clearEmailCodeAttempt()
setMessage({message: "Verification successful", type: "success"})
await onSuccess({user: outcome.user, createdNewRecipeUser: true})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the ok outcome of submitEmailCodeDetailed and all consumers of createdNewRecipeUser.
fd -t f 'client.ts' web/packages/agenta-auth/src --exec cat -n {}
rg -n -C3 'createdNewRecipeUser' web

Repository: Agenta-AI/agenta

Length of output: 24120


🌐 Web query:

SuperTokens Passwordless consumeCode status OK createdNewRecipeUser type user response createdNewRecipeUser

💡 Result:

In the SuperTokens Passwordless recipe, the consumeCode function is used to verify an authentication code [1][2]. When the operation is successful, it returns a response with the status "OK" [1][3]. The field createdNewRecipeUser (or createdNewUser in some API contexts) is a boolean that indicates whether a new user account was created as a result of this authentication attempt [1][4][3]. Key details: - Status "OK": Indicates that the code was valid and consumed successfully [1][4][2]. - createdNewRecipeUser: A boolean value. If true, it means the email address or phone number provided did not previously exist in your SuperTokens user pool, and a new user record was generated [4][3]. If false, the code was used to sign in an existing user [4][2]. - Backend vs. Frontend SDKs: While the field is consistently named createdNewRecipeUser in many Backend SDK definitions (such as Node.js) [1][3], some API endpoints or frontend implementations may refer to it as createdNewUser [4][5][6]. Both serve the same purpose of informing the application whether it is a new user registration or an existing user login [4][3].

Citations:


Forward createdNewRecipeUser from submitEmailCodeDetailed.

submitEmailCodeDetailed already includes createdNewRecipeUser in every successful OTP ok outcome, so the success callback does not need to hardcode it. This prevents the post-auth flow from treating returning users as new user registrations.

-                await onSuccess({user: outcome.user, createdNewRecipeUser: true})
+                await onSuccess({
+                    user: outcome.user,
+                    createdNewRecipeUser: outcome.createdNewRecipeUser,
+                })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (outcome.kind === "ok") {
await clearEmailCodeAttempt()
setMessage({message: "Verification successful", type: "success"})
await onSuccess({user: outcome.user, createdNewRecipeUser: true})
if (outcome.kind === "ok") {
await clearEmailCodeAttempt()
setMessage({message: "Verification successful", type: "success"})
await onSuccess({
user: outcome.user,
createdNewRecipeUser: outcome.createdNewRecipeUser,
})

Comment on lines +21 to +22
/** Render the inline "Last used" tag. */
lastUsed?: boolean

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

lastUsed marks every provider button.

lastUsed is a single flag for the whole list. If providers contains more than one entry, each button shows the "Last used" tag. The tag then states something false. Today the promoted call site passes one provider, so the defect is latent.

Identify the provider instead of the list.

♻️ Proposed change
-    /** Render the inline "Last used" tag. */
-    lastUsed?: boolean
+    /** Provider id that receives the inline "Last used" tag. */
+    lastUsedProviderId?: string
-                    {lastUsed && (
+                    {lastUsedProviderId === provider.id && (
                         <span className="auth-last-used-tag absolute right-3">Last used</span>
                     )}

Also applies to: 56-58

Comment on lines +21 to +30
"dependencies": {
"@agenta/shared": "workspace:../agenta-shared",
"supertokens-web-js": "^0.16.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^20.19.20",
"@vitest/coverage-v8": "^4.1.4",
"typescript": "^5.9.3",
"vitest": "^4.1.4"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Regenerate and commit pnpm-lock.yaml.

The frozen install fails because these dependency specifiers are missing from the lockfile. This blocks linting, formatting, type checks, and tests. Run pnpm install from web and commit the updated lockfile.

Source: Pipeline failures

Comment on lines +288 to +305
export async function submitEmailCodeDetailed(code: string): Promise<OtpDetailedOutcome> {
ensureAuthInit()
try {
const response = await Passwordless.consumeCode({userInputCode: code})
if (response.status === "OK")
return {kind: "ok", user: response.user, createdNewRecipeUser: true}
if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR")
return {
kind: "incorrect",
attemptsLeft:
response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount,
}
if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") return {kind: "expired"}
return {kind: "restart"}
} catch {
return {kind: "restart"}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the consumeCode OK response field and find consumers of createdNewRecipeUser.
fd -t f 'passwordless' node_modules/supertokens-web-js --max-depth 4 2>/dev/null | head
rg -n -C 3 'createdNewRecipeUser' --type=ts --type=tsx --glob '!**/node_modules/**'

Repository: Agenta-AI/agenta

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate client.ts and package files =="
fd -a 'client\.ts$' web/packages/agenta-auth/src 2>/dev/null || true
fd -a 'package\.json$' web/packages/agenta-auth web 2>/dev/null | head -20 || true

echo "== search createdNewRecipeUser and consumeCode =="
rg -n -C 3 'createdNewRecipeUser|consumeCode|Passwordless' web --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.jsx' --glob '*.json' || true

echo "== inspect relevant client.ts section =="
sed -n '1,110p' web/packages/agenta-auth/src/client.ts
sed -n '260,315p' web/packages/agenta-auth/src/client.ts

echo "== inspect package deps for supertokens =="
if [ -f web/packages/agenta-auth/package.json ]; then
  cat web/packages/agenta-auth/package.json
fi

Repository: Agenta-AI/agenta

Length of output: 34153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect OtpVerifyForm callers and handlers =="
sed -n '1,120p' web/packages/agenta-auth-ui/src/OtpVerifyForm.tsx
rg -n -C 4 'OtpVerifyForm|onSuccess|createdNewRecipeUser|PostSignup|post-signup' web/mobile web/oss web/packages/agenta-auth-ui --glob '*.ts' --glob '*.tsx' || true

echo "== check local lockfile for superTokens version/type metadata evidence if present =="
fd -a 'pnpm-lock\.yaml$|package-lock\.json$' web -maxdepth 2 | head -5 || true
if [ -f web/pnpm-lock.yaml ]; then
  rg -n -C 3 'supertokens-(auth-react|web-js|auth-javascript)|`@supertokens/supertokens`|superTokens' web/pnpm-lock.yaml | head -120 || true
fi

Repository: Agenta-AI/agenta

Length of output: 50373


🌐 Web query:

SuperTokens javascript supertokens-web-js Passwordless consumeCode signature createdNewRecipeUser return value

💡 Result:

In the SuperTokens supertokens-web-js SDK, the Passwordless.consumeCode function returns a Promise that resolves to a union type depending on the outcome of the operation [1][2]. When the status is "OK" (indicating a successful authentication), the return value includes the property createdNewRecipeUser, which is a boolean [1][3]. The full return structure for the "OK" status is [1][2]: { status: "OK"; createdNewRecipeUser: boolean; user: User; fetchResponse: Response; } The createdNewRecipeUser field is true if this is the first time the user has signed up for your application, and false if they are an existing user who has signed in [4]. For other possible outcomes, the consumeCode function returns different structures [1][2]: - If the input code is incorrect or expired (status is "INCORRECT_USER_INPUT_CODE_ERROR" or "EXPIRED_USER_INPUT_CODE_ERROR"), it returns the failed attempt count and maximum allowed attempts [1][2]. - If the flow must be restarted (status is "RESTART_FLOW_ERROR"), it returns that status along with the fetch response [1][2]. - If sign-in/up is not allowed (e.g., account-linking conflicts), it returns "SIGN_IN_UP_NOT_ALLOWED" along with a reason string [1][2].

Citations:


Pass createdNewRecipeUser through for OTP consumption.

Passwordless.consumeCode includes createdNewRecipeUser on success, where true means signup and false means an existing user signed in. This wrapper hardcodes true, and OtpVerifyForm passes it into auth success payloads, so returning OTP users can be treated as new-onboarding users.

🐛 Proposed fix
         const response = await Passwordless.consumeCode({userInputCode: code})
         if (response.status === "OK")
-            return {kind: "ok", user: response.user, createdNewRecipeUser: true}
+            return {
+                kind: "ok",
+                user: response.user,
+                createdNewRecipeUser: response.createdNewRecipeUser,
+            }

Also update web/packages/agenta-auth-ui/src/OtpVerifyForm.tsx:73 to pass outcome.createdNewRecipeUser instead of hardcoding true.

</verification result>

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function submitEmailCodeDetailed(code: string): Promise<OtpDetailedOutcome> {
ensureAuthInit()
try {
const response = await Passwordless.consumeCode({userInputCode: code})
if (response.status === "OK")
return {kind: "ok", user: response.user, createdNewRecipeUser: true}
if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR")
return {
kind: "incorrect",
attemptsLeft:
response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount,
}
if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") return {kind: "expired"}
return {kind: "restart"}
} catch {
return {kind: "restart"}
}
}
export async function submitEmailCodeDetailed(code: string): Promise<OtpDetailedOutcome> {
ensureAuthInit()
try {
const response = await Passwordless.consumeCode({userInputCode: code})
if (response.status === "OK")
return {
kind: "ok",
user: response.user,
createdNewRecipeUser: response.createdNewRecipeUser,
}
if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR")
return {
kind: "incorrect",
attemptsLeft:
response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount,
}
if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") return {kind: "expired"}
return {kind: "restart"}
} catch {
return {kind: "restart"}
}
}

…d provider tag

The auth package's plain elements dropped a few things antd used to hand us.

- auth.css: drop the duplicated border: none on .auth-input and restate a
  :focus-visible ring for the input, the surface button, the yellow keycap and
  the quiet button — outline: none had left them with no focus indicator.
- OtpInput: Backspace removed everything after the caret instead of the one
  character under it, so editing cell 2 of ABC123 silently ate 123.
- SocialAuthButtons: lastUsed was one flag for the whole list, so every button
  claimed to be the last used one. It is now lastUsedProviderId and tags the
  provider it names.
- PasswordlessAuthProps: isLoading/setIsLoading were never forwarded to the
  shared form, which owns its own request state. Dropped from the interface and
  both call sites; AuthUpgradeModal's isLoading had no writer left.
- EmailFirstForm / EmailPasswordForm: a rejected callback escaped as an
  unhandled rejection and the user saw nothing. Both now surface a generic
  failure through the error surface they already render.
- aria-label on the email and password inputs — a placeholder is not an
  accessible name.
Deliberate behavior change, not a refactor: the OTP path hardcoded
createdNewRecipeUser: true, so every email-code sign-in looked like a fresh
signup. Passwordless.consumeCode already tells us which it was, so forward the
real value through the three links of the chain — submitEmailCodeDetailed,
OtpVerifyForm and the OSS SendOTP binding.

The flag feeds usePostAuthRedirect.handleAuthSuccess, which prefers the
backend-stamped is_new_user session claim and only falls back to this flag when
reading the access token payload throws. In that fallback on EE a returning user
was sent to /post-signup instead of the workspace they were headed for.

The signUpDetailed and EmailPasswordForm literals stay: a successful
EmailPassword.signUp did create the user. The other OtpVerifyForm consumer,
mobile's SignInScreen, discards the payload.
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