@object-ui/auth 14.1.0 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/LoginForm.d.ts.map +1 -1
- package/dist/LoginForm.js +19 -8
- package/dist/RegisterForm.d.ts.map +1 -1
- package/dist/RegisterForm.js +2 -3
- package/dist/createAuthClient.d.ts.map +1 -1
- package/dist/createAuthClient.js +105 -37
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# @object-ui/auth
|
|
2
2
|
|
|
3
|
+
## 16.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 45c6fb4: Login-page auth-config hardening (#2625, #2626):
|
|
8
|
+
|
|
9
|
+
- `createAuthClient.getConfig` now single-flights + caches the `/auth/config`
|
|
10
|
+
fetch (the login page's three consumers used to fire three requests) and
|
|
11
|
+
retries failures with backoff (500ms/1.5s/3.5s, 8s per-attempt abort) before
|
|
12
|
+
rejecting. A cold-starting environment kernel no longer strands the page
|
|
13
|
+
without its SSO buttons; a final failure clears the cache so later callers
|
|
14
|
+
retry.
|
|
15
|
+
- `LoginForm` holds a spinner instead of painting the password-form defaults
|
|
16
|
+
while config resolves — an SSO-only deployment must never flash a password
|
|
17
|
+
wall at JIT users who have no password. A failed config still falls back to
|
|
18
|
+
the password form (break-glass beats lock-out).
|
|
19
|
+
- `signInWithProvider` gains a 20s watchdog: a sign-in request that hangs now
|
|
20
|
+
rejects with a clear timeout error so the provider button recovers instead
|
|
21
|
+
of spinning forever.
|
|
22
|
+
- Removed LoginForm's duplicate "or" divider — SocialSignInButtons already
|
|
23
|
+
renders its own, and the stacked pair read as a rendering glitch.
|
|
24
|
+
|
|
25
|
+
- 077e45b: `signInWithProvider` with `type: 'oidc'` now signs in through better-auth's
|
|
26
|
+
core social route (`POST /sign-in/social`) and only falls back to the legacy
|
|
27
|
+
`POST /sign-in/oauth2` endpoint when the social route rejects the provider.
|
|
28
|
+
|
|
29
|
+
better-auth ≥ 1.7 restructured the `genericOAuth` plugin: generic OAuth/OIDC
|
|
30
|
+
providers are injected into the core social sign-in flow and the dedicated
|
|
31
|
+
`/sign-in/oauth2` endpoint no longer exists. The old client therefore 404'd on
|
|
32
|
+
every "Continue with ObjectStack" click (platform SSO broken end-to-end on
|
|
33
|
+
current framework). The fallback keeps the button working against older
|
|
34
|
+
(< 1.7) servers during the coordinated rollout; when both routes fail, the
|
|
35
|
+
social-route error is surfaced since on a ≥ 1.7 server it is the real failure.
|
|
36
|
+
|
|
37
|
+
- 022735f: RegisterForm: drop the duplicate "or" divider (matching the LoginForm fix in
|
|
38
|
+
#2629). SocialSignInButtons already renders its own "or continue with email"
|
|
39
|
+
divider under the provider buttons; RegisterForm stacked a second "OR" line on
|
|
40
|
+
top, which read as a rendering glitch on the sign-up page.
|
|
41
|
+
- Updated dependencies [210806a]
|
|
42
|
+
- Updated dependencies [b4ef588]
|
|
43
|
+
- Updated dependencies [5534535]
|
|
44
|
+
- Updated dependencies [9b8f978]
|
|
45
|
+
- @object-ui/types@16.0.0
|
|
46
|
+
|
|
47
|
+
## 15.0.0
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- @object-ui/types@15.0.0
|
|
52
|
+
|
|
3
53
|
## 14.1.0
|
|
4
54
|
|
|
5
55
|
### Patch Changes
|
package/dist/LoginForm.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LoginForm.d.ts","sourceRoot":"","sources":["../src/LoginForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAYtD,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8GAA8G;IAC9G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iGAAiG;IACjG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yHAAyH;IACzH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mCAAmC;IACnC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IAC5D,uCAAuC;IACvC,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,OAAO,EACP,WAAyB,EACzB,iBAAsC,EACtC,KAAiC,EACjC,WAAyD,EACzD,IAAI,EACJ,QAAgB,EAChB,aAAa,EAAE,QAAsB,EACrC,MAAW,EACX,aAAa,GACd,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"LoginForm.d.ts","sourceRoot":"","sources":["../src/LoginForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAYtD,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8GAA8G;IAC9G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iGAAiG;IACjG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yHAAyH;IACzH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mCAAmC;IACnC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IAC5D,uCAAuC;IACvC,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,OAAO,EACP,WAAyB,EACzB,iBAAsC,EACtC,KAAiC,EACjC,WAAyD,EACzD,IAAI,EACJ,QAAgB,EAChB,aAAa,EAAE,QAAsB,EACrC,MAAW,EACX,aAAa,GACd,EAAE,cAAc,qBA4ahB"}
|
package/dist/LoginForm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* ObjectUI
|
|
4
4
|
* Copyright (c) 2024-present ObjectStack Inc.
|
|
@@ -10,7 +10,7 @@ import { useEffect, useState } from 'react';
|
|
|
10
10
|
import { useAuth } from './useAuth';
|
|
11
11
|
import { SocialSignInButtons } from './SocialSignInButtons';
|
|
12
12
|
import { looksLikePhoneIdentifier, normalizePhoneIdentifier } from './phone-identifier';
|
|
13
|
-
import { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS,
|
|
13
|
+
import { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS, AuthErrorBanner, AuthFormHeader, AuthSpinner, } from './authStyles';
|
|
14
14
|
const DefaultLink = ({ href, className, children }) => (_jsx("a", { href: href, className: className, children: children }));
|
|
15
15
|
const DefaultLockIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", className: "h-6 w-6", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2" }), _jsx("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })] }));
|
|
16
16
|
/**
|
|
@@ -70,6 +70,13 @@ export function LoginForm({ onSuccess, onError, registerUrl = '/register', forgo
|
|
|
70
70
|
// state because SSO routing is a raw fetch, not the shared `signIn` (whose
|
|
71
71
|
// `isLoading` drives the password submit button).
|
|
72
72
|
const [ssoSubmitting, setSsoSubmitting] = useState(false);
|
|
73
|
+
// Config not resolved yet: render a spinner INSTEAD of the password-form
|
|
74
|
+
// defaults. Painting the defaults first showed an SSO-only deployment a
|
|
75
|
+
// password wall (no SSO button, no enforced collapse) whenever the config
|
|
76
|
+
// fetch was slow/failed on first load — and platform-SSO JIT users have no
|
|
77
|
+
// password at all (#2625). A FAILED fetch (after the client's retries)
|
|
78
|
+
// still falls back to the password form: break-glass beats lock-out.
|
|
79
|
+
const [configPending, setConfigPending] = useState(true);
|
|
73
80
|
useEffect(() => {
|
|
74
81
|
let cancelled = false;
|
|
75
82
|
Promise.resolve()
|
|
@@ -86,6 +93,10 @@ export function LoginForm({ onSuccess, onError, registerUrl = '/register', forgo
|
|
|
86
93
|
.catch(() => {
|
|
87
94
|
// SSO is an enhancement, not required — leave the buttons/form hidden
|
|
88
95
|
// or shown at their safe defaults.
|
|
96
|
+
})
|
|
97
|
+
.finally(() => {
|
|
98
|
+
if (!cancelled)
|
|
99
|
+
setConfigPending(false);
|
|
89
100
|
});
|
|
90
101
|
return () => {
|
|
91
102
|
cancelled = true;
|
|
@@ -219,10 +230,10 @@ export function LoginForm({ onSuccess, onError, registerUrl = '/register', forgo
|
|
|
219
230
|
setSsoSubmitting(false);
|
|
220
231
|
}
|
|
221
232
|
};
|
|
222
|
-
return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultLockIcon, {})), title: title, description: ssoEnforced && !showPasswordFallback ? l.ssoOnlyDescription : description }),
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
233
|
+
return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultLockIcon, {})), title: title, description: ssoEnforced && !showPasswordFallback ? l.ssoOnlyDescription : description }), _jsx("div", { className: "space-y-5", children: configPending ? (_jsx("div", { className: "flex justify-center py-10", role: "status", "aria-live": "polite", "data-testid": "login-config-loading", children: _jsx(AuthSpinner, {}) })) : (_jsxs(_Fragment, { children: [_jsx(SocialSignInButtons, { mode: "sign-in", onProvidersResolved: (hasProviders) => setHasSocialProviders(hasProviders) }), passwordFormVisible ? (mode === 'phone-otp' ? (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-phone", className: AUTH_FIELD_LABEL_CLASS, children: l.phoneLabel }), _jsx("input", { id: "login-phone", type: "tel", placeholder: l.phonePlaceholder, value: phone, onChange: (e) => setPhone(e.target.value), required: true, autoComplete: "tel", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-otp", className: AUTH_FIELD_LABEL_CLASS, children: l.otpCodeLabel }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { id: "login-otp", type: "text", inputMode: "numeric", placeholder: l.otpCodePlaceholder, value: otpCode, onChange: (e) => setOtpCode(e.target.value), required: true, autoComplete: "one-time-code", disabled: isLoading, className: AUTH_INPUT_CLASS }), _jsxs("button", { type: "button", onClick: handleSendOtp, disabled: otpSending || otpCooldown > 0 || !phone.trim() || isLoading, className: "shrink-0 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50", children: [otpSending && _jsx(AuthSpinner, {}), otpCooldown > 0
|
|
234
|
+
? l.resendOtpCountdownText.replace('{seconds}', String(otpCooldown))
|
|
235
|
+
: l.sendOtpButton] })] })] }), _jsxs("button", { type: "submit", disabled: isLoading || !otpSent, className: AUTH_PRIMARY_BUTTON_CLASS, children: [isLoading && _jsx(AuthSpinner, {}), isLoading ? l.submittingButton : l.submitButton] }), _jsx("button", { type: "button", onClick: () => switchMode('password'), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.usePasswordSignInText })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-email", className: AUTH_FIELD_LABEL_CLASS, children: phonePasswordEnabled ? l.emailOrPhoneLabel : l.emailLabel }), _jsx("input", { id: "login-email",
|
|
236
|
+
// `text` (not `email`) when a phone number is also accepted, so the
|
|
237
|
+
// browser doesn't reject a phone-shaped value on native validation.
|
|
238
|
+
type: phonePasswordEnabled ? 'text' : 'email', inputMode: phonePasswordEnabled ? 'text' : 'email', placeholder: phonePasswordEnabled ? l.emailOrPhonePlaceholder : l.emailPlaceholder, value: email, onChange: (e) => setEmail(e.target.value), required: true, autoComplete: "username", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("label", { htmlFor: "login-password", className: AUTH_FIELD_LABEL_CLASS, children: l.passwordLabel }), forgotPasswordUrl && (_jsx(LinkComp, { href: forgotPasswordUrl, className: "text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.forgotPasswordText }))] }), _jsx("input", { id: "login-password", type: "password", placeholder: l.passwordPlaceholder, value: password, onChange: (e) => setPassword(e.target.value), required: true, autoComplete: "current-password", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("button", { type: "submit", disabled: isLoading, className: AUTH_PRIMARY_BUTTON_CLASS, children: [isLoading && _jsx(AuthSpinner, {}), isLoading ? l.submittingButton : l.submitButton] }), ssoEnabled && (_jsxs("button", { type: "button", onClick: handleSso, disabled: isLoading || ssoSubmitting, "aria-busy": ssoSubmitting, className: "flex w-full items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50", children: [ssoSubmitting && _jsx(AuthSpinner, {}), l.ssoButton] })), phoneOtpEnabled && (_jsx("button", { type: "button", onClick: () => switchMode('phone-otp'), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.usePhoneOtpText })), ssoEnforced && showPasswordFallback && (_jsx("button", { type: "button", onClick: () => setShowPasswordFallback(false), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.backToSsoText }))] }))) : (_jsxs("div", { className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsx("button", { type: "button", onClick: () => setShowPasswordFallback(true), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.usePasswordText })] }))] })) }), registerUrl && !ssoEnforced && (_jsxs("p", { className: "px-8 text-center text-sm text-muted-foreground", children: [l.noAccountText, ' ', _jsx(LinkComp, { href: registerUrl, className: AUTH_LINK_CLASS, children: l.signUpText })] }))] }));
|
|
228
239
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RegisterForm.d.ts","sourceRoot":"","sources":["../src/RegisterForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"RegisterForm.d.ts","sourceRoot":"","sources":["../src/RegisterForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAWtD,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC;sFACkF;IAClF,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;2CAEuC;IACvC,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,qCAAqC;IACrC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IAC5D,uCAAuC;IACvC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAwBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,sBAAsB,EACtB,OAAO,EACP,QAAmB,EACnB,KAA2B,EAC3B,WAAqD,EACrD,IAAI,EACJ,QAAgB,EAChB,aAAa,EAAE,QAAsB,EACrC,MAAW,EACX,aAAa,GACd,EAAE,iBAAiB,qBAoKnB"}
|
package/dist/RegisterForm.js
CHANGED
|
@@ -9,7 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
import { useState } from 'react';
|
|
10
10
|
import { useAuth } from './useAuth';
|
|
11
11
|
import { SocialSignInButtons } from './SocialSignInButtons';
|
|
12
|
-
import { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS,
|
|
12
|
+
import { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS, AuthErrorBanner, AuthFormHeader, AuthSpinner, } from './authStyles';
|
|
13
13
|
const DefaultLink = ({ href, className, children }) => (_jsx("a", { href: href, className: className, children: children }));
|
|
14
14
|
const DefaultUserPlusIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", className: "h-6 w-6", "aria-hidden": "true", children: [_jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }), _jsx("circle", { cx: "9", cy: "7", r: "4" }), _jsx("line", { x1: "19", y1: "8", x2: "19", y2: "14" }), _jsx("line", { x1: "22", y1: "11", x2: "16", y2: "11" })] }));
|
|
15
15
|
/**
|
|
@@ -32,7 +32,6 @@ export function RegisterForm({ onSuccess, onVerificationRequired, onError, login
|
|
|
32
32
|
const [password, setPassword] = useState('');
|
|
33
33
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
34
34
|
const [error, setError] = useState(null);
|
|
35
|
-
const [hasSocialProviders, setHasSocialProviders] = useState(false);
|
|
36
35
|
const l = {
|
|
37
36
|
nameLabel: labels.nameLabel ?? 'Name',
|
|
38
37
|
namePlaceholder: labels.namePlaceholder ?? 'John Doe',
|
|
@@ -76,5 +75,5 @@ export function RegisterForm({ onSuccess, onVerificationRequired, onError, login
|
|
|
76
75
|
onError?.(authError);
|
|
77
76
|
}
|
|
78
77
|
};
|
|
79
|
-
return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultUserPlusIcon, {})), title: title, description: description }), _jsxs("div", { className: "space-y-5", children: [_jsx(SocialSignInButtons, { mode: "sign-up"
|
|
78
|
+
return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultUserPlusIcon, {})), title: title, description: description }), _jsxs("div", { className: "space-y-5", children: [_jsx(SocialSignInButtons, { mode: "sign-up" }), _jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-name", className: AUTH_FIELD_LABEL_CLASS, children: l.nameLabel }), _jsx("input", { id: "register-name", type: "text", placeholder: l.namePlaceholder, value: name, onChange: (e) => setName(e.target.value), required: true, autoComplete: "name", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-email", className: AUTH_FIELD_LABEL_CLASS, children: l.emailLabel }), _jsx("input", { id: "register-email", type: "email", placeholder: l.emailPlaceholder, value: email, onChange: (e) => setEmail(e.target.value), required: true, autoComplete: "email", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-password", className: AUTH_FIELD_LABEL_CLASS, children: l.passwordLabel }), _jsx("input", { id: "register-password", type: "password", placeholder: l.passwordPlaceholder, value: password, onChange: (e) => setPassword(e.target.value), required: true, minLength: 8, autoComplete: "new-password", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-confirm-password", className: AUTH_FIELD_LABEL_CLASS, children: l.confirmPasswordLabel }), _jsx("input", { id: "register-confirm-password", type: "password", placeholder: l.confirmPasswordPlaceholder, value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true, minLength: 8, autoComplete: "new-password", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("button", { type: "submit", disabled: isLoading, className: AUTH_PRIMARY_BUTTON_CLASS, children: [isLoading && _jsx(AuthSpinner, {}), isLoading ? l.submittingButton : l.submitButton] })] })] }), loginUrl && (_jsxs("p", { className: "px-8 text-center text-sm text-muted-foreground", children: [l.hasAccountText, ' ', _jsx(LinkComp, { href: loginUrl, className: AUTH_LINK_CLASS, children: l.signInText })] }))] }));
|
|
80
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createAuthClient.d.ts","sourceRoot":"","sources":["../src/createAuthClient.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EACV,UAAU,EAAE,gBAAgB,EAE7B,MAAM,SAAS,CAAC;AAIjB;;;GAGG;AACH,eAAO,MAAM,YAAY;kBACD,MAAM,GAAG,IAAI;WAE5B,MAAM,GAAG,IAAI;eAST,MAAM,GAAG,IAAI;aASf,IAAI;CAQd,CAAC;AAmHF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"createAuthClient.d.ts","sourceRoot":"","sources":["../src/createAuthClient.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EACV,UAAU,EAAE,gBAAgB,EAE7B,MAAM,SAAS,CAAC;AAIjB;;;GAGG;AACH,eAAO,MAAM,YAAY;kBACD,MAAM,GAAG,IAAI;WAE5B,MAAM,GAAG,IAAI;eAST,MAAM,GAAG,IAAI;aASf,IAAI;CAQd,CAAC;AAmHF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CA2oBrE"}
|
package/dist/createAuthClient.js
CHANGED
|
@@ -196,6 +196,62 @@ export function createAuthClient(config) {
|
|
|
196
196
|
}
|
|
197
197
|
return payload ?? {};
|
|
198
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Fetch `GET {authUrl}/config` once, with a per-attempt timeout so a
|
|
201
|
+
* request that HANGS (a freshly provisioned environment whose kernel is
|
|
202
|
+
* still cold-starting) converts into a retryable failure instead of
|
|
203
|
+
* stalling the login page forever.
|
|
204
|
+
*/
|
|
205
|
+
async function fetchConfigAttempt(timeoutMs) {
|
|
206
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
|
|
207
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
|
|
208
|
+
try {
|
|
209
|
+
const url = `${origin}${basePath}/config`;
|
|
210
|
+
const response = await bearerFetch(url, {
|
|
211
|
+
method: 'GET',
|
|
212
|
+
headers: { 'Content-Type': 'application/json' },
|
|
213
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
214
|
+
});
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
throw new Error(`Failed to load auth config (status ${response.status})`);
|
|
217
|
+
}
|
|
218
|
+
const body = (await response.json());
|
|
219
|
+
// Server wraps the payload as `{ success, data }`; tolerate both shapes.
|
|
220
|
+
if (body && typeof body === 'object' && 'data' in body && body.data) {
|
|
221
|
+
return body.data;
|
|
222
|
+
}
|
|
223
|
+
return body;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
if (timer !== undefined)
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Auth config is static per server boot: single-flight + cache the success
|
|
231
|
+
// so the login page's multiple consumers (LoginForm, SocialSignInButtons,
|
|
232
|
+
// RegisterForm) share ONE request instead of three. Failures RETRY with a
|
|
233
|
+
// short backoff before rejecting — a first-load failure used to render the
|
|
234
|
+
// page without its SSO buttons, which strands SSO-only users on a password
|
|
235
|
+
// wall they have no password for (#2625). A final failure clears the cache
|
|
236
|
+
// so the next caller starts a fresh cycle.
|
|
237
|
+
const CONFIG_RETRY_DELAYS_MS = [500, 1500, 3500];
|
|
238
|
+
const CONFIG_ATTEMPT_TIMEOUT_MS = 8000;
|
|
239
|
+
let configPromise = null;
|
|
240
|
+
async function loadConfigWithRetry() {
|
|
241
|
+
let lastError;
|
|
242
|
+
for (let attempt = 0; attempt <= CONFIG_RETRY_DELAYS_MS.length; attempt++) {
|
|
243
|
+
try {
|
|
244
|
+
return await fetchConfigAttempt(CONFIG_ATTEMPT_TIMEOUT_MS);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
lastError = err;
|
|
248
|
+
if (attempt < CONFIG_RETRY_DELAYS_MS.length) {
|
|
249
|
+
await new Promise((resolve) => setTimeout(resolve, CONFIG_RETRY_DELAYS_MS[attempt]));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
254
|
+
}
|
|
199
255
|
return {
|
|
200
256
|
async signIn(credentials) {
|
|
201
257
|
const { data, error } = await betterAuth.signIn.email({
|
|
@@ -471,61 +527,73 @@ export function createAuthClient(config) {
|
|
|
471
527
|
return (raw && typeof raw === 'object' && 'user' in raw ? raw.user : raw);
|
|
472
528
|
},
|
|
473
529
|
async getConfig() {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (!response.ok) {
|
|
480
|
-
throw new Error(`Failed to load auth config (status ${response.status})`);
|
|
481
|
-
}
|
|
482
|
-
const body = (await response.json());
|
|
483
|
-
// Server wraps the payload as `{ success, data }`; tolerate both shapes.
|
|
484
|
-
if (body && typeof body === 'object' && 'data' in body && body.data) {
|
|
485
|
-
return body.data;
|
|
530
|
+
if (!configPromise) {
|
|
531
|
+
configPromise = loadConfigWithRetry().catch((err) => {
|
|
532
|
+
configPromise = null;
|
|
533
|
+
throw err;
|
|
534
|
+
});
|
|
486
535
|
}
|
|
487
|
-
return
|
|
536
|
+
return configPromise;
|
|
488
537
|
},
|
|
489
538
|
async signInWithProvider(providerId, options = {}) {
|
|
490
539
|
const { type = 'social', callbackURL, errorCallbackURL } = options;
|
|
540
|
+
// Watchdog (#2626): a sign-in request that HANGS (e.g. the environment
|
|
541
|
+
// kernel is cold-starting on first open) used to leave the provider
|
|
542
|
+
// button spinning forever — no error, no way to retry. Convert a
|
|
543
|
+
// no-response into a rejection so the button contract (#2458 pending +
|
|
544
|
+
// inline error) can recover it. The abandoned request may still land
|
|
545
|
+
// later; the user retrying just starts a fresh redirect, which is safe.
|
|
546
|
+
const SIGN_IN_TIMEOUT_MS = 20000;
|
|
547
|
+
const withTimeout = (p) => new Promise((resolve, reject) => {
|
|
548
|
+
const timer = setTimeout(() => reject(new Error('Sign-in timed out — the server did not respond. Please try again.')), SIGN_IN_TIMEOUT_MS);
|
|
549
|
+
p.then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
|
|
550
|
+
});
|
|
491
551
|
// We pass `disableDefaultFetchPlugins: true` to better-auth above,
|
|
492
552
|
// which also disables better-auth's `redirectPlugin` (the one that
|
|
493
553
|
// navigates the browser to `data.url` when the server responds with
|
|
494
554
|
// `{ url, redirect: true }`). The server still returns that payload
|
|
495
555
|
// for OAuth/OIDC providers — we just have to honour it ourselves.
|
|
496
|
-
|
|
497
|
-
const oauth2 = betterAuth.signIn.oauth2;
|
|
498
|
-
if (!oauth2) {
|
|
499
|
-
throw new Error('OIDC sign-in is not supported by this auth client build');
|
|
500
|
-
}
|
|
501
|
-
const { data, error } = await oauth2({ providerId, callbackURL, errorCallbackURL });
|
|
502
|
-
if (error) {
|
|
503
|
-
throw new Error(error.message ?? `Auth request failed with status ${error.status}`);
|
|
504
|
-
}
|
|
505
|
-
// A missing redirect URL means the flow CANNOT continue — surface it
|
|
506
|
-
// instead of resolving silently (the user would see a dead button).
|
|
507
|
-
if (!data?.url) {
|
|
508
|
-
throw new Error(`Sign-in with "${providerId}" did not return a redirect URL — please try again or contact your administrator.`);
|
|
509
|
-
}
|
|
510
|
-
if (typeof window !== 'undefined') {
|
|
511
|
-
window.location.href = data.url;
|
|
512
|
-
}
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
const { data, error } = await betterAuth.signIn.social({
|
|
556
|
+
const signInSocial = async () => await withTimeout(betterAuth.signIn.social({
|
|
516
557
|
provider: providerId,
|
|
517
558
|
callbackURL,
|
|
518
559
|
errorCallbackURL,
|
|
519
|
-
});
|
|
560
|
+
}));
|
|
561
|
+
let result;
|
|
562
|
+
if (type === 'oidc') {
|
|
563
|
+
// better-auth ≥ 1.7 registers generic OAuth/OIDC providers through
|
|
564
|
+
// the core social sign-in flow — `POST /sign-in/social` with the
|
|
565
|
+
// provider id — and no longer mounts the dedicated
|
|
566
|
+
// `/sign-in/oauth2` endpoint. Try the social route first; when it
|
|
567
|
+
// rejects the provider (an older < 1.7 server that only knows OIDC
|
|
568
|
+
// providers via `/sign-in/oauth2`), fall back to the legacy route.
|
|
569
|
+
result = await signInSocial();
|
|
570
|
+
if (result.error) {
|
|
571
|
+
const oauth2 = betterAuth.signIn.oauth2;
|
|
572
|
+
const legacy = oauth2
|
|
573
|
+
? await withTimeout(oauth2({ providerId, callbackURL, errorCallbackURL })).catch(() => null)
|
|
574
|
+
: null;
|
|
575
|
+
if (legacy && !legacy.error) {
|
|
576
|
+
result = legacy;
|
|
577
|
+
}
|
|
578
|
+
// Legacy also failed (or is unavailable) — keep the social-route
|
|
579
|
+
// error below: on a ≥ 1.7 server it is the real failure, while the
|
|
580
|
+
// legacy route only 404s.
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
result = await signInSocial();
|
|
585
|
+
}
|
|
586
|
+
const { data, error } = result;
|
|
520
587
|
if (error) {
|
|
521
588
|
throw new Error(error.message ?? `Auth request failed with status ${error.status}`);
|
|
522
589
|
}
|
|
523
|
-
|
|
524
|
-
|
|
590
|
+
// A missing redirect URL means the flow CANNOT continue — surface it
|
|
591
|
+
// instead of resolving silently (the user would see a dead button).
|
|
592
|
+
if (!data?.url) {
|
|
525
593
|
throw new Error(`Sign-in with "${providerId}" did not return a redirect URL — please try again or contact your administrator.`);
|
|
526
594
|
}
|
|
527
595
|
if (typeof window !== 'undefined') {
|
|
528
|
-
window.location.href =
|
|
596
|
+
window.location.href = data.url;
|
|
529
597
|
}
|
|
530
598
|
},
|
|
531
599
|
// --- Organization / Workspace methods ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Authentication system for Object UI with AuthProvider, useAuth hook, AuthGuard, and form components.",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"better-auth": "^1.6.23",
|
|
34
|
-
"@object-ui/types": "
|
|
34
|
+
"@object-ui/types": "16.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/react": "19.2.17",
|