feat(frontend): @agenta/auth + @agenta/auth-ui — one sign-in layer for oss, ee and mobile - #5865
feat(frontend): @agenta/auth + @agenta/auth-ui — one sign-in layer for oss, ee and mobile#5865ardaerzin wants to merge 3 commits into
Conversation
…r oss, ee and mobile
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughSummary by CodeRabbit
WalkthroughAuthentication logic and UI are extracted into ChangesAuthentication runtime and client
Shared authentication UI
OSS authentication integration
Mobile authentication integration
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 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 |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (13)
web/oss/src/components/pages/auth/assets/useTurnstileSecurity.tsx (1)
22-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueKeep ref mutations out of render.
Line 25 writes to
tokenRef.currentin the render body. Mutating refs during render violates the render-purity rule and can become incompatible with compiler-linted React. Move the assignment into auseEffectkeyed bytoken, or avoidtokenstate 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
useEffectto the React import on line 1.Source: Coding guidelines
web/mobile/src/features/auth/SignInScreen.tsx (2)
35-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the
as AuthMessagecast.The state type at Line 45 is
Partial<AuthMessage>. An empty object already satisfiesPartial<AuthMessage>, so the cast is unnecessary. The cast also asserts a false type for any other consumer ofEMPTY_MESSAGE.♻️ Proposed refactor
-const EMPTY_MESSAGE = {} as AuthMessage +const EMPTY_MESSAGE: Partial<AuthMessage> = {}
107-116: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueMemoize 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,EmailPasswordFormandOtpVerifyFormtherefore cannot skip re-renders.Wrap the provider array in
useMemokeyed onmethods?.providers, and wrapstartProviderand theonSuccessadapters inuseCallback.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, andReact.memowhere 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 valueNote the follow-up and the
lucide-reacthas no Google brand glyph, soGlobedefault.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 valueConsider exporting the auth class names as constants from
@agenta/auth-ui.The change replaces
authQuietButtonClass,authPrimaryButtonClassand the input style constant with literal strings. The strings now appear in this file and inSignInScreen.tsx. A typo inauth-surface-btnno longer fails the build, and a rename inauth.csscannot be traced by the compiler.Exporting the class names from
@agenta/auth-uinext toauth.cssrestores 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 sameauth-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 winAdd 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 setsAGENTA_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
locationheader.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 winReduce 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 winConsider hosting
parseWithLoggingin@agenta/shared.The guidelines require
safeParseWithLoggingfrom@agenta/entities/sharedat API boundaries. The layering note is valid, because@agenta/authmust not depend on@agenta/entities.@agenta/sharedsits 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
safeParseWithLoggingfrom@agenta/entities/shared" and "respect the hierarchyshared ← 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 valueConsider 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 testsvalue.trim()but returns the untrimmedvalue, 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 winSet
maxLengthto 1 and label each cell.Each cell holds one character, but
maxLength={length}allows six characters in a single cell.onFocusselection hides this in most cases; a caret placed without selection still letshandleChangeinsert several characters and shift the value. Paste is not affected, becausehandlePastecallspreventDefault.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 winHandle rejections and re-entry in
resend.
submitwraps its network call intry/catch, butresenddoes not. IfresendEmailCoderejects, 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 winAnnounce 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 tradeoffRaw 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 ontovar(--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
📒 Files selected for processing (49)
web/mobile/src/features/auth/AuthDivider.tsxweb/mobile/src/features/auth/EmailOtpForm.tsxweb/mobile/src/features/auth/EmailPasswordForm.tsxweb/mobile/src/features/auth/OidcProviderButtons.tsxweb/mobile/src/features/auth/SignInScreen.tsxweb/mobile/src/features/auth/SsoDiscoveryForm.tsxweb/mobile/src/features/auth/authStyles.tsweb/mobile/src/features/auth/providerIcons.tsxweb/mobile/src/lib/auth/index.tsweb/mobile/src/middleware.tsweb/mobile/tests/unit/middleware.test.tsweb/oss/src/components/pages/auth/EmailFirst/index.tsxweb/oss/src/components/pages/auth/EmailPasswordAuth/index.tsxweb/oss/src/components/pages/auth/EmailPasswordSignIn/index.tsxweb/oss/src/components/pages/auth/PasswordlessAuth/index.tsxweb/oss/src/components/pages/auth/SendOTP/index.tsxweb/oss/src/components/pages/auth/SocialAuth/index.tsxweb/oss/src/components/pages/auth/assets/ShowErrorMessage.tsxweb/oss/src/components/pages/auth/assets/lastAuthMethod.tsweb/oss/src/components/pages/auth/assets/useTurnstileSecurity.tsxweb/packages/agenta-auth-ui/eslint.config.mjsweb/packages/agenta-auth-ui/package.jsonweb/packages/agenta-auth-ui/src/AuthDivider.tsxweb/packages/agenta-auth-ui/src/EmailFirstForm.tsxweb/packages/agenta-auth-ui/src/EmailPasswordForm.tsxweb/packages/agenta-auth-ui/src/OtpInput.tsxweb/packages/agenta-auth-ui/src/OtpVerifyForm.tsxweb/packages/agenta-auth-ui/src/PasswordlessRequestForm.tsxweb/packages/agenta-auth-ui/src/ShowErrorMessage.tsxweb/packages/agenta-auth-ui/src/SocialAuthButtons.tsxweb/packages/agenta-auth-ui/src/auth.cssweb/packages/agenta-auth-ui/src/index.tsweb/packages/agenta-auth-ui/src/types.tsweb/packages/agenta-auth-ui/tsconfig.jsonweb/packages/agenta-auth/eslint.config.mjsweb/packages/agenta-auth/package.jsonweb/packages/agenta-auth/src/client.tsweb/packages/agenta-auth/src/config.tsweb/packages/agenta-auth/src/discovery.tsweb/packages/agenta-auth/src/index.tsweb/packages/agenta-auth/src/lastAuthMethod.tsweb/packages/agenta-auth/src/otpMachine.tsweb/packages/agenta-auth/src/runtime.tsweb/packages/agenta-auth/tests/unit/authConfig.test.tsweb/packages/agenta-auth/tests/unit/authDiscover.test.tsweb/packages/agenta-auth/tests/unit/lastAuthMethod.test.tsweb/packages/agenta-auth/tests/unit/otpMachine.test.tsweb/packages/agenta-auth/tsconfig.jsonweb/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
| 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"}) | ||
| } |
There was a problem hiding this comment.
🩺 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:
setOidcLoading(false)never runs.oidcLoadingstaystrue, andSocialAuthButtonsstays disabled until the user reloads the page.- 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"})
+ }
}| <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> |
There was a problem hiding this comment.
📐 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 || trueRepository: 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
doneRepository: 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')))
PYRepository: 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
| 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 |
There was a problem hiding this comment.
🩺 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.tsRepository: 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-L3web/oss/src/components/pages/auth/assets/lastAuthMethod.ts#L1-L1
| 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} | ||
| /> |
There was a problem hiding this comment.
🎯 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.tsRepository: 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"
doneRepository: 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
doneRepository: 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.
| 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> | ||
| }} |
There was a problem hiding this comment.
🎯 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.tsRepository: 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:
- 1: https://supertokens.com/docs/nodejs/variables/recipe_passwordless.consumeCode.html
- 2: https://supertokens.com/docs/references/backend-sdks/supertokens-nodejs/recipe-passwordless-types
- 3: https://supertokens-supertokens-core-28.mintlify.app/api/passwordless/consume-code
- 4: https://supertokens.com/docs/auth-react/modules/recipe_passwordless.html
- 5: https://supertokens.com/docs/references/frontend-sdks/supertokens-auth-react/recipe-passwordless
🌐 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:
- 1: https://supertokens.com/docs/references/frontend-sdks/supertokens-auth-react/recipe-passwordless
- 2: https://supertokens.com/docs/auth-react/modules/recipe_passwordless.html
- 3: https://supertokens.com/docs/web-js/modules/recipe_passwordless.html
- 4: https://cdn.jsdelivr.net/npm/supertokens-web-js@0.16.0/lib/build/recipe/passwordless/index.d.ts
- 5: https://cdn.jsdelivr.net/npm/supertokens-web-js@0.16.0/lib/build/recipe/passwordless/types.d.ts
- 6: https://supertokens-supertokens-core-28.mintlify.app/api/passwordless/consume-code
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.
| 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) | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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) | |
| } |
| if (outcome.kind === "ok") { | ||
| await clearEmailCodeAttempt() | ||
| setMessage({message: "Verification successful", type: "success"}) | ||
| await onSuccess({user: outcome.user, createdNewRecipeUser: true}) |
There was a problem hiding this comment.
🗄️ 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' webRepository: 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:
- 1: https://supertokens.com/docs/nodejs/variables/recipe_passwordless.consumeCode.html
- 2: https://mintlify.wiki/supertokens/supertokens-core/auth/passwordless
- 3: https://supertokens.com/docs/references/backend-sdks/supertokens-nodejs/recipe-passwordless-types
- 4: https://supertokens-supertokens-core-28.mintlify.app/api/passwordless/consume-code
- 5: https://supertokens.com/docs/references/frontend-sdks/supertokens-auth-react/recipe-passwordless
- 6: https://apis.io/schemas/supertokens/supertokens-consumepasswordlesscoderesponse/
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.
| 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, | |
| }) |
| /** Render the inline "Last used" tag. */ | ||
| lastUsed?: boolean |
There was a problem hiding this comment.
🎯 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
| "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" |
There was a problem hiding this comment.
📐 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
| 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"} | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ 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
fiRepository: 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
fiRepository: 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:
- 1: https://supertokens.com/docs/references/frontend-sdks/supertokens-auth-react/recipe-passwordless
- 2: https://supertokens.com/docs/auth-react/modules/recipe_passwordless.html
- 3: https://cdn.jsdelivr.net/npm/supertokens-web-js@0.16.0/lib/build/recipe/passwordless/index.d.ts
- 4: https://supertokens.com/docs/authentication/passwordless/hooks-and-overrides
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.
| 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-in existed three times: OSS had it, EE re-implemented it, and
/mhad nothing. This lanemakes it one layer.
@agenta/auth— the headless half: SuperTokens client wiring, runtime/edition config, SSOdiscovery, the OTP state machine, last-used-method persistence. No React, no antd.
@agenta/auth-ui— the rendered half: email-first, email+password, passwordless request, OTPverify, social buttons, the divider and the error strip. antd-free, so
/mcan use it./mgets a realSignInScreenplus 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-fix24/24,tsc --noEmitclean 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.