@object-ui/auth 15.0.0 → 16.1.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 +88 -0
- package/README.md +8 -2
- 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/dist/createAuthenticatedFetch.d.ts +13 -1
- package/dist/createAuthenticatedFetch.d.ts.map +1 -1
- package/dist/createAuthenticatedFetch.js +15 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# @object-ui/auth
|
|
2
2
|
|
|
3
|
+
## 16.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 803558e: feat(data): thread the host's authenticated fetch into `provider: 'api'` data sources (#2725)
|
|
8
|
+
|
|
9
|
+
`provider: 'api'` view data sources went through a bare `globalThis.fetch`, so
|
|
10
|
+
custom endpoints (gantt composite trees, report aggregates) carried only
|
|
11
|
+
same-origin cookies while every native `/api/v1/*` request carried
|
|
12
|
+
`Authorization: Bearer` — the moment cookie HMAC verification failed (dev
|
|
13
|
+
restart rotating the fallback auth secret, cookie expiry/rotation in prod)
|
|
14
|
+
those views 401'd while the rest of the app kept working.
|
|
15
|
+
|
|
16
|
+
- **`@object-ui/react`** — `SchemaRendererProvider` accepts an optional
|
|
17
|
+
`apiFetch`; nested providers inherit it from their parent so re-wrapped
|
|
18
|
+
subtrees (react pages, preview surfaces) keep the host's authentication.
|
|
19
|
+
`useViewData` defaults the api-provider adapter's fetch to the context
|
|
20
|
+
`apiFetch` (explicit `adapterOptions.fetch` still wins).
|
|
21
|
+
- **`@object-ui/auth`** — `createAuthenticatedFetch` gains a
|
|
22
|
+
`sameOriginOnly` option: cross-origin URLs pass through to the bare fetch
|
|
23
|
+
with no `Authorization` / `X-Tenant-ID` / `Accept-Language`, so metadata-
|
|
24
|
+
supplied third-party URLs never see the platform token.
|
|
25
|
+
- **`@object-ui/app-shell`** — the console wires
|
|
26
|
+
`createAuthenticatedFetch({ sameOriginOnly: true })` (settle-signal wrapped)
|
|
27
|
+
as `apiFetch` on the root `SchemaRendererProvider`.
|
|
28
|
+
- **`@object-ui/plugin-gantt`** — `ObjectGantt` resolves its api-provider
|
|
29
|
+
DataSource with the context `apiFetch`, covering reads and write-backs.
|
|
30
|
+
|
|
31
|
+
Behaviour is unchanged for hosts that don't provide `apiFetch` (bare fetch +
|
|
32
|
+
cookies, as before).
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- Updated dependencies [7cf4051]
|
|
37
|
+
- Updated dependencies [94d4876]
|
|
38
|
+
- Updated dependencies [2b17339]
|
|
39
|
+
- Updated dependencies [31b77d4]
|
|
40
|
+
- Updated dependencies [6d4fbe6]
|
|
41
|
+
- Updated dependencies [62b9ab5]
|
|
42
|
+
- Updated dependencies [29c6040]
|
|
43
|
+
- Updated dependencies [faebac3]
|
|
44
|
+
- Updated dependencies [199fa83]
|
|
45
|
+
- @object-ui/types@16.1.0
|
|
46
|
+
|
|
47
|
+
## 16.0.0
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- 45c6fb4: Login-page auth-config hardening (#2625, #2626):
|
|
52
|
+
|
|
53
|
+
- `createAuthClient.getConfig` now single-flights + caches the `/auth/config`
|
|
54
|
+
fetch (the login page's three consumers used to fire three requests) and
|
|
55
|
+
retries failures with backoff (500ms/1.5s/3.5s, 8s per-attempt abort) before
|
|
56
|
+
rejecting. A cold-starting environment kernel no longer strands the page
|
|
57
|
+
without its SSO buttons; a final failure clears the cache so later callers
|
|
58
|
+
retry.
|
|
59
|
+
- `LoginForm` holds a spinner instead of painting the password-form defaults
|
|
60
|
+
while config resolves — an SSO-only deployment must never flash a password
|
|
61
|
+
wall at JIT users who have no password. A failed config still falls back to
|
|
62
|
+
the password form (break-glass beats lock-out).
|
|
63
|
+
- `signInWithProvider` gains a 20s watchdog: a sign-in request that hangs now
|
|
64
|
+
rejects with a clear timeout error so the provider button recovers instead
|
|
65
|
+
of spinning forever.
|
|
66
|
+
- Removed LoginForm's duplicate "or" divider — SocialSignInButtons already
|
|
67
|
+
renders its own, and the stacked pair read as a rendering glitch.
|
|
68
|
+
|
|
69
|
+
- 077e45b: `signInWithProvider` with `type: 'oidc'` now signs in through better-auth's
|
|
70
|
+
core social route (`POST /sign-in/social`) and only falls back to the legacy
|
|
71
|
+
`POST /sign-in/oauth2` endpoint when the social route rejects the provider.
|
|
72
|
+
|
|
73
|
+
better-auth ≥ 1.7 restructured the `genericOAuth` plugin: generic OAuth/OIDC
|
|
74
|
+
providers are injected into the core social sign-in flow and the dedicated
|
|
75
|
+
`/sign-in/oauth2` endpoint no longer exists. The old client therefore 404'd on
|
|
76
|
+
every "Continue with ObjectStack" click (platform SSO broken end-to-end on
|
|
77
|
+
current framework). The fallback keeps the button working against older
|
|
78
|
+
(< 1.7) servers during the coordinated rollout; when both routes fail, the
|
|
79
|
+
social-route error is surfaced since on a ≥ 1.7 server it is the real failure.
|
|
80
|
+
|
|
81
|
+
- 022735f: RegisterForm: drop the duplicate "or" divider (matching the LoginForm fix in
|
|
82
|
+
#2629). SocialSignInButtons already renders its own "or continue with email"
|
|
83
|
+
divider under the provider buttons; RegisterForm stacked a second "OR" line on
|
|
84
|
+
top, which read as a rendering glitch on the sign-up page.
|
|
85
|
+
- Updated dependencies [210806a]
|
|
86
|
+
- Updated dependencies [b4ef588]
|
|
87
|
+
- Updated dependencies [5534535]
|
|
88
|
+
- Updated dependencies [9b8f978]
|
|
89
|
+
- @object-ui/types@16.0.0
|
|
90
|
+
|
|
3
91
|
## 15.0.0
|
|
4
92
|
|
|
5
93
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -126,10 +126,16 @@ Displays current user info with avatar and sign-out:
|
|
|
126
126
|
|
|
127
127
|
### createAuthenticatedFetch
|
|
128
128
|
|
|
129
|
-
Creates a fetch wrapper that injects
|
|
129
|
+
Creates a fetch wrapper that injects the stored Bearer token (plus
|
|
130
|
+
`X-Tenant-ID` and `Accept-Language`) into API requests:
|
|
130
131
|
|
|
131
132
|
```tsx
|
|
132
|
-
const authedFetch = createAuthenticatedFetch(
|
|
133
|
+
const authedFetch = createAuthenticatedFetch();
|
|
134
|
+
|
|
135
|
+
// For fetches whose target URL comes from view metadata (`provider: 'api'`
|
|
136
|
+
// data sources), restrict credential injection to the current page's origin
|
|
137
|
+
// so the platform token never leaks to third-party hosts:
|
|
138
|
+
const apiProviderFetch = createAuthenticatedFetch({ sameOriginOnly: true });
|
|
133
139
|
```
|
|
134
140
|
|
|
135
141
|
## Preview Mode
|
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 ---
|
|
@@ -24,6 +24,18 @@ export declare const ActiveOrganizationStorage: {
|
|
|
24
24
|
set(orgId: string): void;
|
|
25
25
|
clear(): void;
|
|
26
26
|
};
|
|
27
|
+
export interface CreateAuthenticatedFetchOptions {
|
|
28
|
+
/**
|
|
29
|
+
* When true, requests whose URL resolves to a different origin than the
|
|
30
|
+
* current page are passed through to the bare global fetch — no
|
|
31
|
+
* Authorization, X-Tenant-ID, or Accept-Language headers are attached.
|
|
32
|
+
*
|
|
33
|
+
* Use this for fetches whose target URL comes from view metadata
|
|
34
|
+
* (`provider: 'api'` data sources): those may point at third-party hosts
|
|
35
|
+
* that must never see the platform bearer token.
|
|
36
|
+
*/
|
|
37
|
+
sameOriginOnly?: boolean;
|
|
38
|
+
}
|
|
27
39
|
/**
|
|
28
40
|
* Creates an authenticated fetch wrapper that injects the Bearer token
|
|
29
41
|
* from localStorage into every request to the ObjectStack API.
|
|
@@ -42,5 +54,5 @@ export declare const ActiveOrganizationStorage: {
|
|
|
42
54
|
* });
|
|
43
55
|
* ```
|
|
44
56
|
*/
|
|
45
|
-
export declare function createAuthenticatedFetch(): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
57
|
+
export declare function createAuthenticatedFetch(options?: CreateAuthenticatedFetchOptions): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
46
58
|
//# sourceMappingURL=createAuthenticatedFetch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createAuthenticatedFetch.d.ts","sourceRoot":"","sources":["../src/createAuthenticatedFetch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAID;;;GAGG;AACH,eAAO,MAAM,yBAAyB;kBACd,MAAM,GAAG,IAAI;WAE5B,MAAM,GAAG,IAAI;eAST,MAAM,GAAG,IAAI;aASf,IAAI;CAQd,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,
|
|
1
|
+
{"version":3,"file":"createAuthenticatedFetch.d.ts","sourceRoot":"","sources":["../src/createAuthenticatedFetch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAID;;;GAGG;AACH,eAAO,MAAM,yBAAyB;kBACd,MAAM,GAAG,IAAI;WAE5B,MAAM,GAAG,IAAI;eAST,MAAM,GAAG,IAAI;aASf,IAAI;CAQd,CAAC;AAEF,MAAM,WAAW,+BAA+B;IAC9C;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAYD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,CAAC,EAAE,+BAA+B,GACxC,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CA0CrE"}
|
|
@@ -42,6 +42,17 @@ export const ActiveOrganizationStorage = {
|
|
|
42
42
|
catch { /* SSR / test */ }
|
|
43
43
|
},
|
|
44
44
|
};
|
|
45
|
+
/** True when `url` resolves to a different origin than the current page. */
|
|
46
|
+
function isCrossOrigin(url) {
|
|
47
|
+
if (typeof window === 'undefined' || !window.location)
|
|
48
|
+
return true;
|
|
49
|
+
try {
|
|
50
|
+
return new URL(url, window.location.href).origin !== window.location.origin;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
/**
|
|
46
57
|
* Creates an authenticated fetch wrapper that injects the Bearer token
|
|
47
58
|
* from localStorage into every request to the ObjectStack API.
|
|
@@ -60,10 +71,13 @@ export const ActiveOrganizationStorage = {
|
|
|
60
71
|
* });
|
|
61
72
|
* ```
|
|
62
73
|
*/
|
|
63
|
-
export function createAuthenticatedFetch() {
|
|
74
|
+
export function createAuthenticatedFetch(options) {
|
|
64
75
|
return async (input, init) => {
|
|
65
76
|
const headers = new Headers(init?.headers);
|
|
66
77
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
78
|
+
if (options?.sameOriginOnly && isCrossOrigin(url)) {
|
|
79
|
+
return fetch(input, init);
|
|
80
|
+
}
|
|
67
81
|
const isApiCall = /\/api\//i.test(url);
|
|
68
82
|
const token = TokenStorage.get();
|
|
69
83
|
if (token && isApiCall) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.1.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.1.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/react": "19.2.17",
|