@luminaryworks/auth-react 0.3.1
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/LICENSE +133 -0
- package/README.md +64 -0
- package/dist/index.css +220 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +356 -0
- package/dist/index.js +1043 -0
- package/dist/index.js.map +1 -0
- package/dist/style.css +220 -0
- package/dist/style.css.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
(function(){if(typeof document==="undefined")return;var id="lw-auth-react-headless-login";if(document.getElementById(id))return;var s=document.createElement("style");s.id=id;s.textContent="/* src/HeadlessLoginPanel.module.scss */\n.HeadlessLoginPanel_module_panel {\n --lw-auth-theme: #3a84ff;\n --lw-auth-theme-deep: color-mix(in srgb, var(--lw-auth-theme) 72%, #000);\n width: min(100%, 400px);\n padding: 28px 28px 22px;\n border-radius: 16px;\n background: rgba(255, 255, 255, 0.96);\n color: #14212b;\n box-shadow: 0 24px 64px rgba(15, 23, 42, 0.28);\n font-family:\n \"Segoe UI\",\n \"PingFang SC\",\n \"Noto Sans SC\",\n sans-serif;\n}\n.HeadlessLoginPanel_module_brand {\n display: flex;\n gap: 14px;\n align-items: center;\n margin-bottom: 12px;\n}\n.HeadlessLoginPanel_module_logo {\n border-radius: 12px;\n display: block;\n}\n.HeadlessLoginPanel_module_product {\n margin: 0;\n font-size: 13px;\n font-weight: 650;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--lw-auth-theme);\n}\n.HeadlessLoginPanel_module_title {\n margin: 2px 0 0;\n font-size: 22px;\n font-weight: 700;\n line-height: 1.25;\n}\n.HeadlessLoginPanel_module_subtitle {\n margin: 0 0 18px;\n font-size: 14px;\n line-height: 1.5;\n color: #4b5c68;\n}\n.HeadlessLoginPanel_module_form {\n display: grid;\n gap: 10px;\n}\n.HeadlessLoginPanel_module_socialStack {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n font-size: 12px;\n}\n.HeadlessLoginPanel_module_input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid #c9d5de;\n border-radius: 10px;\n padding: 10px 12px;\n font-size: 14px;\n outline: none;\n background: #f7fafc;\n}\n.HeadlessLoginPanel_module_input:focus {\n border-color: var(--lw-auth-theme);\n}\n.HeadlessLoginPanel_module_input:disabled {\n opacity: 0.65;\n cursor: not-allowed;\n}\n.HeadlessLoginPanel_module_passwordField {\n position: relative;\n display: flex;\n align-items: center;\n}\n.HeadlessLoginPanel_module_passwordInput {\n padding-right: 42px;\n}\n.HeadlessLoginPanel_module_passwordToggle {\n position: absolute;\n right: 4px;\n top: 50%;\n transform: translateY(-50%);\n z-index: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border: none;\n border-radius: 4px;\n padding: 0;\n color: #5a6b76;\n background: transparent;\n cursor: pointer;\n}\n.HeadlessLoginPanel_module_passwordToggle:hover:not(:disabled) {\n color: var(--lw-auth-theme);\n background: color-mix(in srgb, var(--lw-auth-theme) 12%, transparent);\n}\n.HeadlessLoginPanel_module_passwordToggle:focus-visible {\n outline: 2px solid var(--lw-auth-theme);\n outline-offset: 1px;\n}\n.HeadlessLoginPanel_module_passwordToggle:disabled {\n opacity: 0.65;\n cursor: not-allowed;\n}\n.HeadlessLoginPanel_module_primaryBtn {\n border: none;\n border-radius: 10px;\n padding: 11px 14px;\n font-size: 14px;\n font-weight: 650;\n cursor: pointer;\n color: #fff;\n background:\n linear-gradient(\n 135deg,\n var(--lw-auth-theme) 0%,\n var(--lw-auth-theme-deep) 100%);\n}\n.HeadlessLoginPanel_module_primaryBtn:disabled {\n opacity: 0.65;\n cursor: not-allowed;\n}\n.HeadlessLoginPanel_module_socialBtn {\n flex: 1 1 calc(50% - 5px);\n min-width: min(140px, 100%);\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n border: 1px solid #b7c9d4;\n border-radius: 10px;\n padding: 11px 10px;\n font-size: 14px;\n font-weight: 650;\n cursor: pointer;\n color: #14212b;\n background: #fff;\n}\n.HeadlessLoginPanel_module_socialBtn:disabled {\n opacity: 0.65;\n cursor: not-allowed;\n}\n.HeadlessLoginPanel_module_socialLogo {\n flex-shrink: 0;\n display: block;\n}\n.HeadlessLoginPanel_module_logoFallback {\n width: 18px;\n height: 18px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--lw-auth-theme) 14%, #fff);\n color: var(--lw-auth-theme-deep);\n font-size: 11px;\n font-weight: 700;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n.HeadlessLoginPanel_module_cancelBtn {\n margin-top: 10px;\n width: 100%;\n border: none;\n background: transparent;\n color: #5a6b76;\n font-size: 13px;\n cursor: pointer;\n padding: 8px;\n}\n.HeadlessLoginPanel_module_cancelBtn:disabled {\n opacity: 0.65;\n cursor: not-allowed;\n}\n.HeadlessLoginPanel_module_divider {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 14px 0;\n color: #90a0ab;\n}\n.HeadlessLoginPanel_module_dividerLine {\n flex: 1;\n height: 1px;\n background: #d5e0e7;\n}\n.HeadlessLoginPanel_module_dividerText {\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n}\n.HeadlessLoginPanel_module_hint {\n margin: 10px 0 0;\n font-size: 12px;\n color: #6b7c87;\n line-height: 1.45;\n}\n.HeadlessLoginPanel_module_error {\n margin: 12px 0 0;\n font-size: 13px;\n color: #b42318;\n line-height: 1.4;\n}\n.HeadlessLoginPanel_module_footer {\n margin-top: 18px;\n padding-top: 12px;\n border-top: 1px solid #e4ebf0;\n font-size: 12px;\n color: #6b7c87;\n display: flex;\n gap: 8px;\n justify-content: center;\n}\n/*# sourceMappingURL=index.css.map */";document.head.appendChild(s);})();
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var oidcClientTs = require('oidc-client-ts');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/types.ts
|
|
9
|
+
function isIdpConfigured(config) {
|
|
10
|
+
return Boolean(config.issuer && config.clientId && config.redirectUri);
|
|
11
|
+
}
|
|
12
|
+
function resolveIssuer(env) {
|
|
13
|
+
const explicit = env.VITE_IDP_ISSUER ?? env.PUBLIC_IDP_ISSUER ?? env.IDP_ISSUER;
|
|
14
|
+
if (explicit) return explicit;
|
|
15
|
+
const gateway = env.VITE_AUTH_GATEWAY_URL ?? env.PUBLIC_AUTH_GATEWAY_URL ?? env.AUTH_GATEWAY_URL;
|
|
16
|
+
if (gateway) return `${gateway.replace(/\/$/, "")}/oidc`;
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
function resolveExperienceApiBase(env) {
|
|
20
|
+
const explicit = env.VITE_AUTH_EXPERIENCE_URL ?? env.PUBLIC_AUTH_EXPERIENCE_URL ?? env.AUTH_EXPERIENCE_URL;
|
|
21
|
+
if (explicit) return explicit.replace(/\/$/, "");
|
|
22
|
+
const gateway = env.VITE_AUTH_GATEWAY_URL ?? env.PUBLIC_AUTH_GATEWAY_URL ?? env.AUTH_GATEWAY_URL;
|
|
23
|
+
if (gateway) return gateway.replace(/\/$/, "");
|
|
24
|
+
const issuer = resolveIssuer(env);
|
|
25
|
+
if (!issuer) return void 0;
|
|
26
|
+
return issuer.replace(/\/$/, "").replace(/\/oidc$/i, "");
|
|
27
|
+
}
|
|
28
|
+
function readIdpConfigFromEnv(env) {
|
|
29
|
+
const redirectUri = env.VITE_IDP_REDIRECT_URI ?? env.PUBLIC_IDP_REDIRECT_URI ?? "";
|
|
30
|
+
return {
|
|
31
|
+
issuer: resolveIssuer(env),
|
|
32
|
+
clientId: env.VITE_IDP_CLIENT_ID ?? env.PUBLIC_IDP_CLIENT_ID,
|
|
33
|
+
redirectUri,
|
|
34
|
+
popupRedirectUri: env.VITE_IDP_POPUP_REDIRECT_URI ?? env.PUBLIC_IDP_POPUP_REDIRECT_URI ?? redirectUri,
|
|
35
|
+
postLogoutRedirectUri: env.VITE_IDP_POST_LOGOUT_URI ?? env.PUBLIC_IDP_POST_LOGOUT_URI,
|
|
36
|
+
scopes: env.VITE_IDP_SCOPES ?? env.PUBLIC_IDP_SCOPES ?? "openid profile email offline_access",
|
|
37
|
+
audience: env.VITE_IDP_AUDIENCE ?? env.PUBLIC_IDP_AUDIENCE,
|
|
38
|
+
tokenStorageKey: env.VITE_IDP_TOKEN_KEY ?? "luminary_access_token",
|
|
39
|
+
experienceApiBase: resolveExperienceApiBase(env)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
var manager = null;
|
|
43
|
+
function storageKey(config) {
|
|
44
|
+
return config.tokenStorageKey ?? "luminary_access_token";
|
|
45
|
+
}
|
|
46
|
+
function popupRedirectUri(config) {
|
|
47
|
+
return config.popupRedirectUri ?? config.redirectUri;
|
|
48
|
+
}
|
|
49
|
+
function sharedStateStore() {
|
|
50
|
+
return new oidcClientTs.WebStorageStateStore({ store: window.localStorage });
|
|
51
|
+
}
|
|
52
|
+
function clientSettings(config) {
|
|
53
|
+
const resource = config.audience?.trim() || void 0;
|
|
54
|
+
return {
|
|
55
|
+
authority: config.issuer.replace(/\/$/, ""),
|
|
56
|
+
metadataUrl: resolveOidcMetadataUrl(config),
|
|
57
|
+
client_id: config.clientId,
|
|
58
|
+
redirect_uri: config.redirectUri,
|
|
59
|
+
post_logout_redirect_uri: config.postLogoutRedirectUri ?? config.redirectUri,
|
|
60
|
+
response_type: "code",
|
|
61
|
+
scope: config.scopes ?? "openid profile email offline_access",
|
|
62
|
+
stateStore: sharedStateStore(),
|
|
63
|
+
extraQueryParams: resource ? { resource } : void 0,
|
|
64
|
+
extraTokenParams: resource ? { resource } : void 0
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function userManagerSettings(config) {
|
|
68
|
+
const resource = config.audience?.trim() || void 0;
|
|
69
|
+
return {
|
|
70
|
+
...clientSettings(config),
|
|
71
|
+
popup_redirect_uri: popupRedirectUri(config),
|
|
72
|
+
automaticSilentRenew: true,
|
|
73
|
+
loadUserInfo: !resource,
|
|
74
|
+
userStore: sharedStateStore()
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function createUserManager(config) {
|
|
78
|
+
if (manager) return manager;
|
|
79
|
+
manager = new oidcClientTs.UserManager(userManagerSettings(config));
|
|
80
|
+
return manager;
|
|
81
|
+
}
|
|
82
|
+
function resolveOidcMetadataUrl(config) {
|
|
83
|
+
const proxyBase = config.experienceApiBase?.replace(/\/$/, "");
|
|
84
|
+
if (!proxyBase) return void 0;
|
|
85
|
+
try {
|
|
86
|
+
if (new URL(proxyBase).origin === new URL(config.issuer).origin) return void 0;
|
|
87
|
+
} catch {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
return `${proxyBase}/oidc/.well-known/openid-configuration`;
|
|
91
|
+
}
|
|
92
|
+
function resetUserManager() {
|
|
93
|
+
manager = null;
|
|
94
|
+
}
|
|
95
|
+
function signInArgs(config, options) {
|
|
96
|
+
const normalized = typeof options === "string" ? { returnUrl: options } : options ?? {};
|
|
97
|
+
const resource = config.audience?.trim();
|
|
98
|
+
const extraQueryParams = {};
|
|
99
|
+
if (resource) extraQueryParams.resource = resource;
|
|
100
|
+
if (normalized.directSignIn) extraQueryParams.direct_sign_in = normalized.directSignIn;
|
|
101
|
+
return {
|
|
102
|
+
state: normalized.returnUrl ? { returnUrl: normalized.returnUrl } : void 0,
|
|
103
|
+
extraQueryParams: Object.keys(extraQueryParams).length ? extraQueryParams : void 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function signInRedirect(config, returnUrlOrOptions) {
|
|
107
|
+
resetUserManager();
|
|
108
|
+
const um = createUserManager(config);
|
|
109
|
+
await um.signinRedirect(signInArgs(config, returnUrlOrOptions));
|
|
110
|
+
}
|
|
111
|
+
async function prepareSignInRequestUrl(config, returnUrlOrOptions) {
|
|
112
|
+
resetUserManager();
|
|
113
|
+
createUserManager(config);
|
|
114
|
+
const client = new oidcClientTs.OidcClient(clientSettings(config));
|
|
115
|
+
const args = signInArgs(config, returnUrlOrOptions);
|
|
116
|
+
const request = await client.createSigninRequest({
|
|
117
|
+
state: args.state,
|
|
118
|
+
extraQueryParams: {
|
|
119
|
+
...args.extraQueryParams ?? {},
|
|
120
|
+
prompt: "login"
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return request.url;
|
|
124
|
+
}
|
|
125
|
+
async function signInPopup(config, returnUrlOrOptions) {
|
|
126
|
+
resetUserManager();
|
|
127
|
+
const um = createUserManager(config);
|
|
128
|
+
const user = await um.signinPopup(signInArgs(config, returnUrlOrOptions));
|
|
129
|
+
const session = toSession(user);
|
|
130
|
+
if (!session.accessToken) {
|
|
131
|
+
throw new Error("IdP did not return access_token (check audience / API resource)");
|
|
132
|
+
}
|
|
133
|
+
assertJwtAccessToken(session.accessToken);
|
|
134
|
+
localStorage.setItem(storageKey(config), session.accessToken);
|
|
135
|
+
const stateReturnUrl = typeof user.state === "object" && user.state && "returnUrl" in user.state ? String(user.state.returnUrl ?? "") : void 0;
|
|
136
|
+
return { session, returnUrl: stateReturnUrl || void 0 };
|
|
137
|
+
}
|
|
138
|
+
async function handleSignInCallback(config) {
|
|
139
|
+
const um = createUserManager(config);
|
|
140
|
+
const user = await um.signinRedirectCallback();
|
|
141
|
+
const session = toSession(user);
|
|
142
|
+
assertJwtAccessToken(session.accessToken);
|
|
143
|
+
localStorage.setItem(storageKey(config), session.accessToken);
|
|
144
|
+
const returnUrl = typeof user.state === "object" && user.state && "returnUrl" in user.state ? String(user.state.returnUrl ?? "") : void 0;
|
|
145
|
+
return { session, returnUrl: returnUrl || void 0 };
|
|
146
|
+
}
|
|
147
|
+
async function handleSignInPopupCallback(config) {
|
|
148
|
+
const um = createUserManager(config);
|
|
149
|
+
await um.signinPopupCallback();
|
|
150
|
+
}
|
|
151
|
+
function isOidcPopupWindow() {
|
|
152
|
+
try {
|
|
153
|
+
return Boolean(window.opener && window.opener !== window);
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function signOutRedirect(config) {
|
|
159
|
+
localStorage.removeItem(storageKey(config));
|
|
160
|
+
const um = createUserManager(config);
|
|
161
|
+
await um.signoutRedirect();
|
|
162
|
+
}
|
|
163
|
+
function getStoredAccessToken(config) {
|
|
164
|
+
return localStorage.getItem(storageKey(config));
|
|
165
|
+
}
|
|
166
|
+
function clearStoredSession(config) {
|
|
167
|
+
localStorage.removeItem(storageKey(config));
|
|
168
|
+
}
|
|
169
|
+
function toSession(user) {
|
|
170
|
+
return {
|
|
171
|
+
accessToken: user.access_token,
|
|
172
|
+
idToken: user.id_token,
|
|
173
|
+
expiresAt: user.expires_at ? user.expires_at * 1e3 : void 0,
|
|
174
|
+
profile: user.profile
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function assertJwtAccessToken(accessToken) {
|
|
178
|
+
if (accessToken.split(".").length !== 3) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
"Received non-JWT access_token. Pass resource (audience) on authorize/token and register the API resource at the IdP."
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function getCurrentUser(config) {
|
|
185
|
+
const um = createUserManager(config);
|
|
186
|
+
const user = await um.getUser();
|
|
187
|
+
if (!user || user.expired) return null;
|
|
188
|
+
return toSession(user);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/experience-client.ts
|
|
192
|
+
function experienceUrl(apiBase, path) {
|
|
193
|
+
return `${apiBase.replace(/\/$/, "")}/api/experience${path}`;
|
|
194
|
+
}
|
|
195
|
+
async function experienceFetch(apiBase, path, init) {
|
|
196
|
+
const res = await fetch(experienceUrl(apiBase, path), {
|
|
197
|
+
credentials: "include",
|
|
198
|
+
...init,
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
Accept: "application/json",
|
|
202
|
+
...init.headers ?? {}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
const text = await res.text();
|
|
206
|
+
let data = null;
|
|
207
|
+
if (text) {
|
|
208
|
+
try {
|
|
209
|
+
data = JSON.parse(text);
|
|
210
|
+
} catch {
|
|
211
|
+
data = text;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
const msg = data && typeof data === "object" && data !== null && "message" in data ? String(data.message) : `Experience API ${res.status}`;
|
|
216
|
+
throw new Error(msg);
|
|
217
|
+
}
|
|
218
|
+
return data;
|
|
219
|
+
}
|
|
220
|
+
function guessIdentifierType(identifier) {
|
|
221
|
+
if (identifier.includes("@")) return "email";
|
|
222
|
+
if (/^\+?\d{6,}$/.test(identifier.trim())) return "phone";
|
|
223
|
+
return "username";
|
|
224
|
+
}
|
|
225
|
+
function sameOriginAuthorizeUrl(authorizeUrl, apiBase) {
|
|
226
|
+
const u = new URL(authorizeUrl);
|
|
227
|
+
const base = new URL(apiBase);
|
|
228
|
+
if (u.origin === base.origin) return authorizeUrl;
|
|
229
|
+
return `${base.origin}${u.pathname}${u.search}${u.hash}`;
|
|
230
|
+
}
|
|
231
|
+
async function bootstrapOidcInteraction(input) {
|
|
232
|
+
if (!input.issuer || !input.clientId || !input.redirectUri) return;
|
|
233
|
+
if (typeof document === "undefined") return;
|
|
234
|
+
const config = {
|
|
235
|
+
issuer: input.issuer,
|
|
236
|
+
clientId: input.clientId,
|
|
237
|
+
redirectUri: input.redirectUri,
|
|
238
|
+
audience: input.audience,
|
|
239
|
+
scopes: input.scopes,
|
|
240
|
+
experienceApiBase: input.apiBase
|
|
241
|
+
};
|
|
242
|
+
const signinUrl = await prepareSignInRequestUrl(config, {
|
|
243
|
+
returnUrl: input.returnUrl
|
|
244
|
+
});
|
|
245
|
+
const authorizeUrl = sameOriginAuthorizeUrl(signinUrl, input.apiBase);
|
|
246
|
+
await new Promise((resolve) => {
|
|
247
|
+
const iframe = document.createElement("iframe");
|
|
248
|
+
iframe.setAttribute("aria-hidden", "true");
|
|
249
|
+
iframe.tabIndex = -1;
|
|
250
|
+
iframe.style.cssText = "position:absolute;width:0;height:0;border:0;visibility:hidden";
|
|
251
|
+
let settled = false;
|
|
252
|
+
const finish = () => {
|
|
253
|
+
if (settled) return;
|
|
254
|
+
settled = true;
|
|
255
|
+
iframe.remove();
|
|
256
|
+
resolve();
|
|
257
|
+
};
|
|
258
|
+
iframe.addEventListener("load", () => {
|
|
259
|
+
window.setTimeout(finish, 400);
|
|
260
|
+
});
|
|
261
|
+
iframe.src = authorizeUrl;
|
|
262
|
+
document.body.appendChild(iframe);
|
|
263
|
+
window.setTimeout(finish, 2500);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async function experiencePasswordSignIn(input) {
|
|
267
|
+
const { apiBase, identifier, password } = input;
|
|
268
|
+
await bootstrapOidcInteraction(input);
|
|
269
|
+
const primary = input.identifierType ?? guessIdentifierType(identifier);
|
|
270
|
+
const alternates = primary === "phone" ? [primary] : primary === "email" ? ["email", "username"] : ["username", "email"];
|
|
271
|
+
let lastError;
|
|
272
|
+
for (const identifierType of alternates) {
|
|
273
|
+
try {
|
|
274
|
+
await experienceFetch(apiBase, "", {
|
|
275
|
+
method: "PUT",
|
|
276
|
+
body: JSON.stringify({ interactionEvent: "SignIn" })
|
|
277
|
+
});
|
|
278
|
+
const verified = await experienceFetch(
|
|
279
|
+
apiBase,
|
|
280
|
+
"/verification/password",
|
|
281
|
+
{
|
|
282
|
+
method: "POST",
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
identifier: { type: identifierType, value: identifier },
|
|
285
|
+
password
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
const verificationId = verified?.verificationId;
|
|
290
|
+
if (!verificationId) {
|
|
291
|
+
throw new Error("Experience API did not return verificationId");
|
|
292
|
+
}
|
|
293
|
+
await experienceFetch(apiBase, "/identification", {
|
|
294
|
+
method: "POST",
|
|
295
|
+
body: JSON.stringify({ verificationId })
|
|
296
|
+
});
|
|
297
|
+
const submitted = await experienceFetch(apiBase, "/submit", {
|
|
298
|
+
method: "POST",
|
|
299
|
+
body: JSON.stringify({})
|
|
300
|
+
});
|
|
301
|
+
return { redirectTo: submitted?.redirectTo, raw: submitted };
|
|
302
|
+
} catch (e) {
|
|
303
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
throw lastError ?? new Error("Experience password sign-in failed");
|
|
307
|
+
}
|
|
308
|
+
async function fetchSocialConnectors(input) {
|
|
309
|
+
const url = new URL(`${input.apiBase.replace(/\/$/, "")}/api/.well-known/sign-in-exp`);
|
|
310
|
+
if (input.appId?.trim()) url.searchParams.set("appId", input.appId.trim());
|
|
311
|
+
const res = await fetch(url.toString(), {
|
|
312
|
+
// Public metadata — omit cookies so browsers do not require credentialed CORS.
|
|
313
|
+
credentials: "omit",
|
|
314
|
+
headers: { Accept: "application/json" }
|
|
315
|
+
});
|
|
316
|
+
if (!res.ok) {
|
|
317
|
+
throw new Error(`sign-in-exp ${res.status}`);
|
|
318
|
+
}
|
|
319
|
+
const data = await res.json();
|
|
320
|
+
return (data.socialConnectors ?? []).filter(
|
|
321
|
+
(c) => Boolean(c?.id && c?.target)
|
|
322
|
+
).map((c) => ({
|
|
323
|
+
id: c.id,
|
|
324
|
+
target: c.target,
|
|
325
|
+
name: localizeName(c.name) || titleCase(c.target),
|
|
326
|
+
logo: c.logo
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
function localizeName(name) {
|
|
330
|
+
if (!name) return "";
|
|
331
|
+
if (typeof name === "string") return name;
|
|
332
|
+
return name["zh-CN"] || name.en || Object.values(name)[0] || "";
|
|
333
|
+
}
|
|
334
|
+
function titleCase(target) {
|
|
335
|
+
if (!target) return "";
|
|
336
|
+
return target.charAt(0).toUpperCase() + target.slice(1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/auth-gate.ts
|
|
340
|
+
var AuthGateCoordinator = class {
|
|
341
|
+
phase = "idle";
|
|
342
|
+
lastError;
|
|
343
|
+
/** Cached for useSyncExternalStore — must keep referential equality when unchanged. */
|
|
344
|
+
snapshot = { phase: "idle", blocked: false };
|
|
345
|
+
config = {};
|
|
346
|
+
flight = null;
|
|
347
|
+
reauthWaiters = [];
|
|
348
|
+
listeners = /* @__PURE__ */ new Set();
|
|
349
|
+
configure(config) {
|
|
350
|
+
this.config = { ...this.config, ...config };
|
|
351
|
+
}
|
|
352
|
+
getSnapshot = () => this.snapshot;
|
|
353
|
+
subscribe = (listener) => {
|
|
354
|
+
this.listeners.add(listener);
|
|
355
|
+
return () => {
|
|
356
|
+
this.listeners.delete(listener);
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
emit() {
|
|
360
|
+
for (const listener of this.listeners) listener();
|
|
361
|
+
}
|
|
362
|
+
setPhase(phase, lastError) {
|
|
363
|
+
if (this.phase === phase && this.lastError === lastError) return;
|
|
364
|
+
this.phase = phase;
|
|
365
|
+
this.lastError = lastError;
|
|
366
|
+
this.snapshot = {
|
|
367
|
+
phase,
|
|
368
|
+
blocked: phase !== "idle",
|
|
369
|
+
lastError
|
|
370
|
+
};
|
|
371
|
+
this.emit();
|
|
372
|
+
}
|
|
373
|
+
/** Await while a recovery is in flight (pause outbound API traffic). */
|
|
374
|
+
async waitIfBlocked() {
|
|
375
|
+
if (!this.flight) return;
|
|
376
|
+
await this.flight;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Single-flight 401 handler.
|
|
380
|
+
* Concurrent callers share one recovery; all receive the same disposition.
|
|
381
|
+
*/
|
|
382
|
+
handleUnauthorized(errorMessage) {
|
|
383
|
+
if (this.flight) return this.flight;
|
|
384
|
+
this.flight = this.runRecovery(errorMessage).finally(() => {
|
|
385
|
+
this.flight = null;
|
|
386
|
+
});
|
|
387
|
+
return this.flight;
|
|
388
|
+
}
|
|
389
|
+
/** Open reauth UI without a preceding 401 (cold start / deep link). */
|
|
390
|
+
requestReauth(errorMessage) {
|
|
391
|
+
return this.handleUnauthorized(errorMessage);
|
|
392
|
+
}
|
|
393
|
+
/** Product calls after popup / Headless login restored the session. */
|
|
394
|
+
completeReauth() {
|
|
395
|
+
if (this.phase !== "reauth") return;
|
|
396
|
+
const waiters = this.reauthWaiters.splice(0, this.reauthWaiters.length);
|
|
397
|
+
this.setPhase("idle");
|
|
398
|
+
this.config.onSettled?.(true);
|
|
399
|
+
for (const w of waiters) w.resolve("retry");
|
|
400
|
+
}
|
|
401
|
+
/** User dismissed reauth; fail queued recovery waiters. */
|
|
402
|
+
cancelReauth(errorMessage) {
|
|
403
|
+
if (this.phase !== "reauth") return;
|
|
404
|
+
const waiters = this.reauthWaiters.splice(0, this.reauthWaiters.length);
|
|
405
|
+
this.setPhase("idle", errorMessage);
|
|
406
|
+
this.config.onSettled?.(false);
|
|
407
|
+
for (const w of waiters) w.resolve("fail");
|
|
408
|
+
}
|
|
409
|
+
async runRecovery(errorMessage) {
|
|
410
|
+
this.lastError = errorMessage;
|
|
411
|
+
if (this.config.tryRefresh) {
|
|
412
|
+
this.setPhase("refreshing", errorMessage);
|
|
413
|
+
try {
|
|
414
|
+
const ok = await this.config.tryRefresh();
|
|
415
|
+
if (ok) {
|
|
416
|
+
this.setPhase("idle");
|
|
417
|
+
this.config.onSettled?.(true);
|
|
418
|
+
return "retry";
|
|
419
|
+
}
|
|
420
|
+
} catch (e) {
|
|
421
|
+
this.lastError = e instanceof Error ? e.message : String(e);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.setPhase("reauth", this.lastError);
|
|
425
|
+
this.config.onReauthRequired?.();
|
|
426
|
+
return new Promise((resolve) => {
|
|
427
|
+
this.reauthWaiters.push({ resolve });
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
var authGate = new AuthGateCoordinator();
|
|
432
|
+
async function withAuthGateRetry(execute, options) {
|
|
433
|
+
if (options?.bypass) return execute();
|
|
434
|
+
await authGate.waitIfBlocked();
|
|
435
|
+
try {
|
|
436
|
+
return await execute();
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (!(options?.isUnauthorized ?? defaultIsUnauthorized)(error)) throw error;
|
|
439
|
+
const disposition = await authGate.handleUnauthorized(messageOf(error));
|
|
440
|
+
if (disposition !== "retry") throw error;
|
|
441
|
+
return execute();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function defaultIsUnauthorized(error) {
|
|
445
|
+
if (!error || typeof error !== "object") return false;
|
|
446
|
+
const e = error;
|
|
447
|
+
return e.status === 401 || e.statusCode === 401 || e.response?.status === 401;
|
|
448
|
+
}
|
|
449
|
+
function messageOf(error) {
|
|
450
|
+
if (error instanceof Error) return error.message;
|
|
451
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
452
|
+
const m = error.message;
|
|
453
|
+
return typeof m === "string" ? m : void 0;
|
|
454
|
+
}
|
|
455
|
+
return void 0;
|
|
456
|
+
}
|
|
457
|
+
var LuminaryAuthContext = react.createContext(null);
|
|
458
|
+
function LuminaryAuthProvider({ config: configProp, children }) {
|
|
459
|
+
const config = react.useMemo(() => configProp ?? {}, [configProp]);
|
|
460
|
+
const configured = isIdpConfigured(config);
|
|
461
|
+
const [session, setSession] = react.useState(null);
|
|
462
|
+
const [loading, setLoading] = react.useState(configured);
|
|
463
|
+
const refreshSession = react.useCallback(async () => {
|
|
464
|
+
if (!configured) {
|
|
465
|
+
setSession(null);
|
|
466
|
+
setLoading(false);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
setLoading(true);
|
|
470
|
+
try {
|
|
471
|
+
const current = await getCurrentUser(config);
|
|
472
|
+
setSession(current);
|
|
473
|
+
} catch {
|
|
474
|
+
setSession(null);
|
|
475
|
+
} finally {
|
|
476
|
+
setLoading(false);
|
|
477
|
+
}
|
|
478
|
+
}, [config, configured]);
|
|
479
|
+
react.useEffect(() => {
|
|
480
|
+
void refreshSession();
|
|
481
|
+
}, [refreshSession]);
|
|
482
|
+
const login = react.useCallback(
|
|
483
|
+
async (returnUrl) => {
|
|
484
|
+
if (!configured) throw new Error("IdP not configured");
|
|
485
|
+
await signInRedirect(config, returnUrl);
|
|
486
|
+
},
|
|
487
|
+
[config, configured]
|
|
488
|
+
);
|
|
489
|
+
const loginPopup = react.useCallback(
|
|
490
|
+
async (returnUrl) => {
|
|
491
|
+
if (!configured) throw new Error("IdP not configured");
|
|
492
|
+
const { session: next } = await signInPopup(config, returnUrl);
|
|
493
|
+
setSession(next);
|
|
494
|
+
return next;
|
|
495
|
+
},
|
|
496
|
+
[config, configured]
|
|
497
|
+
);
|
|
498
|
+
const logout = react.useCallback(async () => {
|
|
499
|
+
if (!configured) {
|
|
500
|
+
clearStoredSession(config);
|
|
501
|
+
setSession(null);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
setSession(null);
|
|
505
|
+
await signOutRedirect(config);
|
|
506
|
+
}, [config, configured]);
|
|
507
|
+
const completeCallback = react.useCallback(async () => {
|
|
508
|
+
if (!configured) throw new Error("IdP not configured");
|
|
509
|
+
const { session: next, returnUrl } = await handleSignInCallback(config);
|
|
510
|
+
setSession(next);
|
|
511
|
+
return { returnUrl };
|
|
512
|
+
}, [config, configured]);
|
|
513
|
+
const value = react.useMemo(
|
|
514
|
+
() => ({
|
|
515
|
+
configured,
|
|
516
|
+
session,
|
|
517
|
+
accessToken: session?.accessToken ?? (configured ? getStoredAccessToken(config) : null),
|
|
518
|
+
loading,
|
|
519
|
+
login,
|
|
520
|
+
loginPopup,
|
|
521
|
+
logout,
|
|
522
|
+
completeCallback,
|
|
523
|
+
refreshSession
|
|
524
|
+
}),
|
|
525
|
+
[configured, session, loading, login, loginPopup, logout, completeCallback, refreshSession, config]
|
|
526
|
+
);
|
|
527
|
+
return /* @__PURE__ */ jsxRuntime.jsx(LuminaryAuthContext.Provider, { value, children });
|
|
528
|
+
}
|
|
529
|
+
function useLuminaryAuth() {
|
|
530
|
+
const ctx = react.useContext(LuminaryAuthContext);
|
|
531
|
+
if (!ctx) throw new Error("useLuminaryAuth must be used within LuminaryAuthProvider");
|
|
532
|
+
return ctx;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/HeadlessLoginPanel.module.scss
|
|
536
|
+
var HeadlessLoginPanel_module_default = {
|
|
537
|
+
panel: "HeadlessLoginPanel_module_panel",
|
|
538
|
+
brand: "HeadlessLoginPanel_module_brand",
|
|
539
|
+
logo: "HeadlessLoginPanel_module_logo",
|
|
540
|
+
product: "HeadlessLoginPanel_module_product",
|
|
541
|
+
title: "HeadlessLoginPanel_module_title",
|
|
542
|
+
subtitle: "HeadlessLoginPanel_module_subtitle",
|
|
543
|
+
form: "HeadlessLoginPanel_module_form",
|
|
544
|
+
socialStack: "HeadlessLoginPanel_module_socialStack",
|
|
545
|
+
input: "HeadlessLoginPanel_module_input",
|
|
546
|
+
passwordField: "HeadlessLoginPanel_module_passwordField",
|
|
547
|
+
passwordInput: "HeadlessLoginPanel_module_passwordInput",
|
|
548
|
+
passwordToggle: "HeadlessLoginPanel_module_passwordToggle",
|
|
549
|
+
primaryBtn: "HeadlessLoginPanel_module_primaryBtn",
|
|
550
|
+
socialBtn: "HeadlessLoginPanel_module_socialBtn",
|
|
551
|
+
socialLogo: "HeadlessLoginPanel_module_socialLogo",
|
|
552
|
+
logoFallback: "HeadlessLoginPanel_module_logoFallback",
|
|
553
|
+
cancelBtn: "HeadlessLoginPanel_module_cancelBtn",
|
|
554
|
+
divider: "HeadlessLoginPanel_module_divider",
|
|
555
|
+
dividerLine: "HeadlessLoginPanel_module_dividerLine",
|
|
556
|
+
dividerText: "HeadlessLoginPanel_module_dividerText",
|
|
557
|
+
hint: "HeadlessLoginPanel_module_hint",
|
|
558
|
+
error: "HeadlessLoginPanel_module_error",
|
|
559
|
+
footer: "HeadlessLoginPanel_module_footer"
|
|
560
|
+
};
|
|
561
|
+
var DEFAULT_LOGIN_THEME_COLOR = "#3a84ff";
|
|
562
|
+
var defaults = {
|
|
563
|
+
title: "Sign in",
|
|
564
|
+
subtitle: "Use your LuminaryWorks unified account",
|
|
565
|
+
identifierPlaceholder: "Email or username",
|
|
566
|
+
passwordPlaceholder: "Password",
|
|
567
|
+
submitPassword: "Sign in with password",
|
|
568
|
+
submitSso: "Continue with unified account",
|
|
569
|
+
submitGoogle: "Google",
|
|
570
|
+
submitGithub: "GitHub",
|
|
571
|
+
socialDivider: "or",
|
|
572
|
+
hint: "Social providers open directly. Password uses your LuminaryWorks account.",
|
|
573
|
+
cancel: "Cancel",
|
|
574
|
+
experienceUnavailable: "Password sign-in is unavailable; use a social provider instead.",
|
|
575
|
+
showPassword: "Show password",
|
|
576
|
+
hidePassword: "Hide password"
|
|
577
|
+
};
|
|
578
|
+
var FALLBACK_SOCIAL = [
|
|
579
|
+
{ id: "google", target: "google", name: "Google" },
|
|
580
|
+
{ id: "github", target: "github", name: "GitHub" }
|
|
581
|
+
];
|
|
582
|
+
function resolveSieBase(config) {
|
|
583
|
+
const explicit = config.experienceApiBase?.trim();
|
|
584
|
+
if (explicit) return explicit.replace(/\/$/, "");
|
|
585
|
+
if (config.issuer) {
|
|
586
|
+
try {
|
|
587
|
+
return new URL(config.issuer).origin;
|
|
588
|
+
} catch {
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return void 0;
|
|
592
|
+
}
|
|
593
|
+
function labelFor(connector, labels) {
|
|
594
|
+
if (connector.target === "google" && labels.submitGoogle) return labels.submitGoogle;
|
|
595
|
+
if (connector.target === "github" && labels.submitGithub) return labels.submitGithub;
|
|
596
|
+
return connector.name;
|
|
597
|
+
}
|
|
598
|
+
function cx(...parts) {
|
|
599
|
+
return parts.filter(Boolean).join(" ");
|
|
600
|
+
}
|
|
601
|
+
function HeadlessLoginPanel({
|
|
602
|
+
config,
|
|
603
|
+
productName,
|
|
604
|
+
logoSrc,
|
|
605
|
+
labels: labelsProp,
|
|
606
|
+
returnUrl,
|
|
607
|
+
mode = "popup",
|
|
608
|
+
showSocialConnectors = true,
|
|
609
|
+
socialProviders = "auto",
|
|
610
|
+
showCancel,
|
|
611
|
+
onCancel,
|
|
612
|
+
onOidcSession,
|
|
613
|
+
onExperienceRedirect,
|
|
614
|
+
footer,
|
|
615
|
+
className,
|
|
616
|
+
style,
|
|
617
|
+
themeColor = DEFAULT_LOGIN_THEME_COLOR
|
|
618
|
+
}) {
|
|
619
|
+
const labels = { ...defaults, ...labelsProp };
|
|
620
|
+
const configured = isIdpConfigured(config);
|
|
621
|
+
const [identifier, setIdentifier] = react.useState("");
|
|
622
|
+
const [password, setPassword] = react.useState("");
|
|
623
|
+
const [passwordVisible, setPasswordVisible] = react.useState(false);
|
|
624
|
+
const panelStyle = {
|
|
625
|
+
...style,
|
|
626
|
+
["--lw-auth-theme"]: themeColor || DEFAULT_LOGIN_THEME_COLOR
|
|
627
|
+
};
|
|
628
|
+
const [loading, setLoading] = react.useState(null);
|
|
629
|
+
const [error, setError] = react.useState("");
|
|
630
|
+
const [connectors, setConnectors] = react.useState([]);
|
|
631
|
+
const socialEnabled = showSocialConnectors !== false && !(Array.isArray(socialProviders) && socialProviders.length === 0);
|
|
632
|
+
react.useEffect(() => {
|
|
633
|
+
const resetBusy = () => setLoading(null);
|
|
634
|
+
window.addEventListener("pageshow", resetBusy);
|
|
635
|
+
window.addEventListener("popstate", resetBusy);
|
|
636
|
+
return () => {
|
|
637
|
+
window.removeEventListener("pageshow", resetBusy);
|
|
638
|
+
window.removeEventListener("popstate", resetBusy);
|
|
639
|
+
};
|
|
640
|
+
}, []);
|
|
641
|
+
react.useEffect(() => {
|
|
642
|
+
if (!socialEnabled) {
|
|
643
|
+
setConnectors([]);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
let cancelled = false;
|
|
647
|
+
const sieBase = resolveSieBase(config);
|
|
648
|
+
const allowlist = socialProviders === "auto" || socialProviders === void 0 ? null : new Set(socialProviders.map((t) => t.toLowerCase()));
|
|
649
|
+
const apply = (list) => {
|
|
650
|
+
const filtered = allowlist ? list.filter((c) => allowlist.has(c.target.toLowerCase())) : list;
|
|
651
|
+
if (!cancelled) setConnectors(filtered);
|
|
652
|
+
};
|
|
653
|
+
if (!sieBase) {
|
|
654
|
+
apply(
|
|
655
|
+
allowlist ? FALLBACK_SOCIAL.filter((c) => allowlist.has(c.target)) : FALLBACK_SOCIAL
|
|
656
|
+
);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
void fetchSocialConnectors({ apiBase: sieBase, appId: config.clientId }).then((list) => {
|
|
660
|
+
if (list.length) {
|
|
661
|
+
apply(list);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
apply(
|
|
665
|
+
allowlist ? FALLBACK_SOCIAL.filter((c) => allowlist.has(c.target)) : FALLBACK_SOCIAL
|
|
666
|
+
);
|
|
667
|
+
}).catch(() => {
|
|
668
|
+
apply(
|
|
669
|
+
allowlist ? FALLBACK_SOCIAL.filter((c) => allowlist.has(c.target)) : FALLBACK_SOCIAL
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
return () => {
|
|
673
|
+
cancelled = true;
|
|
674
|
+
};
|
|
675
|
+
}, [config.clientId, config.experienceApiBase, config.issuer, socialEnabled, socialProviders]);
|
|
676
|
+
const runOidc = async (directSignIn) => {
|
|
677
|
+
if (!configured) throw new Error("IdP not configured");
|
|
678
|
+
if (mode === "redirect") {
|
|
679
|
+
await signInRedirect(config, { returnUrl, directSignIn });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const { session, returnUrl: next } = await signInPopup(config, { returnUrl, directSignIn });
|
|
683
|
+
await onOidcSession?.(session, next);
|
|
684
|
+
};
|
|
685
|
+
const runSocial = async (target) => {
|
|
686
|
+
setError("");
|
|
687
|
+
setLoading(target);
|
|
688
|
+
try {
|
|
689
|
+
await runOidc(`social:${target}`);
|
|
690
|
+
} catch (e) {
|
|
691
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
692
|
+
setLoading(null);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
const followExperienceRedirect = (redirectTo) => {
|
|
696
|
+
if (onExperienceRedirect) {
|
|
697
|
+
onExperienceRedirect(redirectTo);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
window.location.assign(redirectTo);
|
|
701
|
+
};
|
|
702
|
+
const runPassword = async (event) => {
|
|
703
|
+
event.preventDefault();
|
|
704
|
+
if (!configured) return;
|
|
705
|
+
setError("");
|
|
706
|
+
setLoading("password");
|
|
707
|
+
try {
|
|
708
|
+
const apiBase = config.experienceApiBase?.trim();
|
|
709
|
+
if (!apiBase) {
|
|
710
|
+
throw new Error(labels.experienceUnavailable);
|
|
711
|
+
}
|
|
712
|
+
const result = await experiencePasswordSignIn({
|
|
713
|
+
apiBase,
|
|
714
|
+
identifier: identifier.trim(),
|
|
715
|
+
password,
|
|
716
|
+
issuer: config.issuer,
|
|
717
|
+
clientId: config.clientId,
|
|
718
|
+
redirectUri: config.redirectUri,
|
|
719
|
+
audience: config.audience,
|
|
720
|
+
scopes: config.scopes,
|
|
721
|
+
returnUrl
|
|
722
|
+
});
|
|
723
|
+
if (result.redirectTo) {
|
|
724
|
+
followExperienceRedirect(result.redirectTo);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
throw new Error("Experience API did not return redirectTo");
|
|
728
|
+
} catch (e) {
|
|
729
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
730
|
+
setLoading(null);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
const busy = loading !== null;
|
|
734
|
+
const showSocial = socialEnabled && connectors.length > 0;
|
|
735
|
+
const showHint = Boolean(labelsProp?.hint) || showSocial;
|
|
736
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx(HeadlessLoginPanel_module_default.panel, className), style: panelStyle, children: [
|
|
737
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: HeadlessLoginPanel_module_default.brand, children: [
|
|
738
|
+
logoSrc ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoSrc, alt: "", width: 48, height: 48, className: HeadlessLoginPanel_module_default.logo }) : null,
|
|
739
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
740
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: HeadlessLoginPanel_module_default.product, children: productName }),
|
|
741
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: HeadlessLoginPanel_module_default.title, children: labels.title })
|
|
742
|
+
] })
|
|
743
|
+
] }),
|
|
744
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: HeadlessLoginPanel_module_default.subtitle, children: labels.subtitle }),
|
|
745
|
+
configured ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
746
|
+
/* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: (e) => void runPassword(e), className: HeadlessLoginPanel_module_default.form, children: [
|
|
747
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
748
|
+
"input",
|
|
749
|
+
{
|
|
750
|
+
type: "text",
|
|
751
|
+
name: "identifier",
|
|
752
|
+
autoComplete: "username",
|
|
753
|
+
placeholder: labels.identifierPlaceholder,
|
|
754
|
+
value: identifier,
|
|
755
|
+
onChange: (e) => setIdentifier(e.target.value),
|
|
756
|
+
className: HeadlessLoginPanel_module_default.input,
|
|
757
|
+
disabled: busy
|
|
758
|
+
}
|
|
759
|
+
),
|
|
760
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: HeadlessLoginPanel_module_default.passwordField, children: [
|
|
761
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
762
|
+
"input",
|
|
763
|
+
{
|
|
764
|
+
type: passwordVisible ? "text" : "password",
|
|
765
|
+
name: "password",
|
|
766
|
+
autoComplete: "current-password",
|
|
767
|
+
placeholder: labels.passwordPlaceholder,
|
|
768
|
+
value: password,
|
|
769
|
+
onChange: (e) => setPassword(e.target.value),
|
|
770
|
+
className: cx(HeadlessLoginPanel_module_default.input, HeadlessLoginPanel_module_default.passwordInput),
|
|
771
|
+
disabled: busy
|
|
772
|
+
}
|
|
773
|
+
),
|
|
774
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
775
|
+
"button",
|
|
776
|
+
{
|
|
777
|
+
type: "button",
|
|
778
|
+
className: HeadlessLoginPanel_module_default.passwordToggle,
|
|
779
|
+
disabled: busy,
|
|
780
|
+
"aria-label": passwordVisible ? labels.hidePassword : labels.showPassword,
|
|
781
|
+
"aria-pressed": passwordVisible,
|
|
782
|
+
onClick: () => setPasswordVisible((v) => !v),
|
|
783
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(PasswordVisibilityIcon, { visible: passwordVisible })
|
|
784
|
+
}
|
|
785
|
+
)
|
|
786
|
+
] }),
|
|
787
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: HeadlessLoginPanel_module_default.primaryBtn, disabled: busy || !identifier || !password, children: loading === "password" ? "\u2026" : labels.submitPassword })
|
|
788
|
+
] }),
|
|
789
|
+
showHint ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: HeadlessLoginPanel_module_default.hint, children: labels.hint }) : null,
|
|
790
|
+
showSocial ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: HeadlessLoginPanel_module_default.divider, "aria-hidden": true, children: [
|
|
791
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: HeadlessLoginPanel_module_default.dividerLine }),
|
|
792
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: HeadlessLoginPanel_module_default.dividerText, children: labels.socialDivider }),
|
|
793
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: HeadlessLoginPanel_module_default.dividerLine })
|
|
794
|
+
] }) : null,
|
|
795
|
+
showSocial ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: HeadlessLoginPanel_module_default.socialStack, children: connectors.map((connector) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
796
|
+
"button",
|
|
797
|
+
{
|
|
798
|
+
type: "button",
|
|
799
|
+
className: HeadlessLoginPanel_module_default.socialBtn,
|
|
800
|
+
disabled: busy,
|
|
801
|
+
onClick: () => void runSocial(connector.target),
|
|
802
|
+
children: [
|
|
803
|
+
/* @__PURE__ */ jsxRuntime.jsx(SocialLogo, { connector }),
|
|
804
|
+
loading === connector.target ? "\u2026" : labelFor(connector, labels)
|
|
805
|
+
]
|
|
806
|
+
},
|
|
807
|
+
connector.id
|
|
808
|
+
)) }) : null
|
|
809
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: HeadlessLoginPanel_module_default.error, children: "IdP is not configured (issuer / clientId / redirectUri)." }),
|
|
810
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: HeadlessLoginPanel_module_default.error, children: error }) : null,
|
|
811
|
+
showCancel ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: HeadlessLoginPanel_module_default.cancelBtn, disabled: busy, onClick: onCancel, children: labels.cancel }) : null,
|
|
812
|
+
footer ? /* @__PURE__ */ jsxRuntime.jsx("footer", { className: HeadlessLoginPanel_module_default.footer, children: footer }) : null
|
|
813
|
+
] });
|
|
814
|
+
}
|
|
815
|
+
function SocialLogo({ connector }) {
|
|
816
|
+
if (connector.logo) {
|
|
817
|
+
return /* @__PURE__ */ jsxRuntime.jsx("img", { src: connector.logo, alt: "", width: 18, height: 18, className: HeadlessLoginPanel_module_default.socialLogo });
|
|
818
|
+
}
|
|
819
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: HeadlessLoginPanel_module_default.logoFallback, "aria-hidden": true, children: connector.name.slice(0, 1).toUpperCase() });
|
|
820
|
+
}
|
|
821
|
+
function PasswordVisibilityIcon({ visible }) {
|
|
822
|
+
if (visible) {
|
|
823
|
+
return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 1024 1024", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M942.2 486.2C847.4 286.5 704.1 186 512 186c-57.9 0-111.5 9.8-160.3 27.7l72.3 72.3C457 273.3 483.5 266 512 266c161.3 0 279.4 81.8 362.7 254-26.3 54.4-57.5 100.2-93.3 136.1l59.6 59.6C891.4 655.7 932.5 578.5 942.2 537.7a60.3 60.3 0 000-51.5zM336.6 278.6l-59.9-59.9C230.6 255.4 182.8 296.4 141.9 348.1 95.4 412.1 64.6 486.5 51.8 512a60.3 60.3 0 000 51.5C115.3 719.5 224.6 814.8 365.4 860.1l72.5-72.5C353.6 761.6 286.3 698.3 241.3 612.5 220.6 573.1 205 528.6 194.5 486.2c24.5-50.4 55.1-92.1 90.8-124.9l51.3-51.3zM606.6 556.6A112 112 0 01467.4 417.4L606.6 556.6zM508 624c-26.5 0-50.9-9.2-70.2-24.6l-59.9 59.9A175.3 175.3 0 00508 688c97.2 0 176-78.8 176-176 0-26.5-9.2-50.9-24.6-70.2l-59.9 59.9c15.4 19.3 24.6 43.7 24.6 70.2 0 61.9-50.1 112-112 112zM880 112L112 880l48 48 768-768z" }) });
|
|
824
|
+
}
|
|
825
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 1024 1024", fill: "currentColor", "aria-hidden": true, children: [
|
|
826
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3a60.3 60.3 0 000-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766z" }),
|
|
827
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M508 336c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z" })
|
|
828
|
+
] });
|
|
829
|
+
}
|
|
830
|
+
function ReauthOverlay({
|
|
831
|
+
config,
|
|
832
|
+
productName,
|
|
833
|
+
logoSrc,
|
|
834
|
+
labels,
|
|
835
|
+
returnUrl,
|
|
836
|
+
themeColor,
|
|
837
|
+
onOidcSession,
|
|
838
|
+
children,
|
|
839
|
+
dismissible = true,
|
|
840
|
+
onDismiss
|
|
841
|
+
}) {
|
|
842
|
+
const snapshot = react.useSyncExternalStore(authGate.subscribe, authGate.getSnapshot, authGate.getSnapshot);
|
|
843
|
+
if (snapshot.phase === "idle") return null;
|
|
844
|
+
const resolvedReturnUrl = returnUrl ?? (() => {
|
|
845
|
+
if (typeof window === "undefined") return void 0;
|
|
846
|
+
const hash = window.location.hash.replace(/^#/, "");
|
|
847
|
+
if (!hash || hash.startsWith("/login") || hash.startsWith("/auth/")) return void 0;
|
|
848
|
+
return hash.startsWith("/") ? hash : `/${hash}`;
|
|
849
|
+
})();
|
|
850
|
+
const handleSession = async (session, next) => {
|
|
851
|
+
await onOidcSession(session, next);
|
|
852
|
+
authGate.completeReauth();
|
|
853
|
+
};
|
|
854
|
+
const handleCancel = () => {
|
|
855
|
+
onDismiss?.();
|
|
856
|
+
authGate.cancelReauth("cancelled");
|
|
857
|
+
};
|
|
858
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
859
|
+
"div",
|
|
860
|
+
{
|
|
861
|
+
role: "dialog",
|
|
862
|
+
"aria-modal": "true",
|
|
863
|
+
"aria-label": productName,
|
|
864
|
+
style: backdropStyle,
|
|
865
|
+
onClick: dismissible ? handleCancel : void 0,
|
|
866
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: centerStyle, onClick: (e) => e.stopPropagation(), children: snapshot.phase === "refreshing" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: spinnerCardStyle, children: [
|
|
867
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: spinnerStyle }),
|
|
868
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0, color: "#e8eef2" }, children: "Refreshing session\u2026" })
|
|
869
|
+
] }) : children ? children : /* @__PURE__ */ jsxRuntime.jsx(
|
|
870
|
+
HeadlessLoginPanel,
|
|
871
|
+
{
|
|
872
|
+
config,
|
|
873
|
+
productName,
|
|
874
|
+
logoSrc,
|
|
875
|
+
themeColor,
|
|
876
|
+
labels: {
|
|
877
|
+
title: labels?.title ?? "Session expired",
|
|
878
|
+
subtitle: labels?.subtitle ?? "Sign in again to keep working \u2014 this page will not reload.",
|
|
879
|
+
...labels
|
|
880
|
+
},
|
|
881
|
+
returnUrl: resolvedReturnUrl,
|
|
882
|
+
mode: "popup",
|
|
883
|
+
showCancel: dismissible,
|
|
884
|
+
onCancel: handleCancel,
|
|
885
|
+
onOidcSession: handleSession
|
|
886
|
+
}
|
|
887
|
+
) })
|
|
888
|
+
}
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
var backdropStyle = {
|
|
892
|
+
position: "fixed",
|
|
893
|
+
inset: 0,
|
|
894
|
+
zIndex: 1e4,
|
|
895
|
+
display: "flex",
|
|
896
|
+
alignItems: "center",
|
|
897
|
+
justifyContent: "center",
|
|
898
|
+
padding: 24,
|
|
899
|
+
background: "radial-gradient(1200px 600px at 20% 10%, rgba(56, 120, 150, 0.35), transparent 55%), rgba(10, 18, 24, 0.72)",
|
|
900
|
+
backdropFilter: "blur(6px)"
|
|
901
|
+
};
|
|
902
|
+
var centerStyle = {
|
|
903
|
+
width: "min(100%, 420px)"
|
|
904
|
+
};
|
|
905
|
+
var spinnerCardStyle = {
|
|
906
|
+
display: "grid",
|
|
907
|
+
justifyItems: "center",
|
|
908
|
+
gap: 14,
|
|
909
|
+
padding: 28,
|
|
910
|
+
borderRadius: 16,
|
|
911
|
+
background: "rgba(20, 32, 40, 0.88)"
|
|
912
|
+
};
|
|
913
|
+
var spinnerStyle = {
|
|
914
|
+
width: 36,
|
|
915
|
+
height: 36,
|
|
916
|
+
borderRadius: "50%",
|
|
917
|
+
border: "3px solid rgba(255,255,255,0.2)",
|
|
918
|
+
borderTopColor: "#7ec8e3",
|
|
919
|
+
animation: "luminary-auth-spin 0.8s linear infinite"
|
|
920
|
+
};
|
|
921
|
+
if (typeof document !== "undefined" && !document.getElementById("luminary-auth-spin-style")) {
|
|
922
|
+
const style = document.createElement("style");
|
|
923
|
+
style.id = "luminary-auth-spin-style";
|
|
924
|
+
style.textContent = `@keyframes luminary-auth-spin { to { transform: rotate(360deg); } }`;
|
|
925
|
+
document.head.appendChild(style);
|
|
926
|
+
}
|
|
927
|
+
var AuthGateContext = react.createContext(null);
|
|
928
|
+
function AuthGateProvider({
|
|
929
|
+
children,
|
|
930
|
+
tryRefresh,
|
|
931
|
+
onReauthRequired,
|
|
932
|
+
onSettled,
|
|
933
|
+
overlay
|
|
934
|
+
}) {
|
|
935
|
+
react.useEffect(() => {
|
|
936
|
+
authGate.configure({ tryRefresh, onReauthRequired, onSettled });
|
|
937
|
+
}, [tryRefresh, onReauthRequired, onSettled]);
|
|
938
|
+
const snapshot = react.useSyncExternalStore(authGate.subscribe, authGate.getSnapshot, authGate.getSnapshot);
|
|
939
|
+
const value = react.useMemo(
|
|
940
|
+
() => ({
|
|
941
|
+
snapshot,
|
|
942
|
+
requestReauth: (message) => authGate.requestReauth(message),
|
|
943
|
+
completeReauth: () => authGate.completeReauth(),
|
|
944
|
+
cancelReauth: (message) => authGate.cancelReauth(message)
|
|
945
|
+
}),
|
|
946
|
+
[snapshot]
|
|
947
|
+
);
|
|
948
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(AuthGateContext.Provider, { value, children: [
|
|
949
|
+
children,
|
|
950
|
+
overlay ? /* @__PURE__ */ jsxRuntime.jsx(ReauthOverlay, { ...overlay }) : null
|
|
951
|
+
] });
|
|
952
|
+
}
|
|
953
|
+
function useAuthGate() {
|
|
954
|
+
const ctx = react.useContext(AuthGateContext);
|
|
955
|
+
if (!ctx) throw new Error("useAuthGate must be used within AuthGateProvider");
|
|
956
|
+
return ctx;
|
|
957
|
+
}
|
|
958
|
+
function useAuthGateOptional() {
|
|
959
|
+
return react.useContext(AuthGateContext);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/post-login-path.ts
|
|
963
|
+
var DEFAULT_UNSAFE = ["/login", "/auth", "/exception"];
|
|
964
|
+
function normalizeAppPath(path, fallback) {
|
|
965
|
+
const raw = path.replace(/^#/, "").trim();
|
|
966
|
+
if (!raw) return fallback.startsWith("/") ? fallback : `/${fallback}`;
|
|
967
|
+
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
968
|
+
}
|
|
969
|
+
function isUnsafeReturnPath(path, unsafePrefixes, fallback) {
|
|
970
|
+
const p = normalizeAppPath(path, fallback);
|
|
971
|
+
if (p === "/") return true;
|
|
972
|
+
return unsafePrefixes.some((prefix) => p === prefix || p.startsWith(`${prefix}/`) || p.startsWith(prefix));
|
|
973
|
+
}
|
|
974
|
+
function createPostLoginPathHelpers(options) {
|
|
975
|
+
const storageKey2 = options.storageKey;
|
|
976
|
+
const defaultPath = options.defaultPath?.startsWith("/") ? options.defaultPath : `/${options.defaultPath || "home"}`;
|
|
977
|
+
const unsafePrefixes = [...DEFAULT_UNSAFE, ...options.unsafePrefixes || []];
|
|
978
|
+
function rememberPostLoginPath(path) {
|
|
979
|
+
const normalized = normalizeAppPath(path, defaultPath);
|
|
980
|
+
if (isUnsafeReturnPath(normalized, unsafePrefixes, defaultPath)) return;
|
|
981
|
+
try {
|
|
982
|
+
sessionStorage.setItem(storageKey2, normalized);
|
|
983
|
+
} catch {
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function peekPostLoginPath() {
|
|
987
|
+
try {
|
|
988
|
+
const v = sessionStorage.getItem(storageKey2);
|
|
989
|
+
if (!v || isUnsafeReturnPath(v, unsafePrefixes, defaultPath)) return void 0;
|
|
990
|
+
return normalizeAppPath(v, defaultPath);
|
|
991
|
+
} catch {
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function consumePostLoginPath(fallback = defaultPath) {
|
|
996
|
+
const remembered = peekPostLoginPath();
|
|
997
|
+
try {
|
|
998
|
+
sessionStorage.removeItem(storageKey2);
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
return remembered ?? normalizeAppPath(fallback, defaultPath);
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
rememberPostLoginPath,
|
|
1005
|
+
peekPostLoginPath,
|
|
1006
|
+
consumePostLoginPath,
|
|
1007
|
+
normalizeAppPath: (path) => normalizeAppPath(path, defaultPath),
|
|
1008
|
+
isUnsafeReturnPath: (path) => isUnsafeReturnPath(path, unsafePrefixes, defaultPath)
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
exports.AuthGateProvider = AuthGateProvider;
|
|
1013
|
+
exports.DEFAULT_LOGIN_THEME_COLOR = DEFAULT_LOGIN_THEME_COLOR;
|
|
1014
|
+
exports.HeadlessLoginPanel = HeadlessLoginPanel;
|
|
1015
|
+
exports.LuminaryAuthProvider = LuminaryAuthProvider;
|
|
1016
|
+
exports.ReauthOverlay = ReauthOverlay;
|
|
1017
|
+
exports.authGate = authGate;
|
|
1018
|
+
exports.clearStoredSession = clearStoredSession;
|
|
1019
|
+
exports.createPostLoginPathHelpers = createPostLoginPathHelpers;
|
|
1020
|
+
exports.createUserManager = createUserManager;
|
|
1021
|
+
exports.experiencePasswordSignIn = experiencePasswordSignIn;
|
|
1022
|
+
exports.fetchSocialConnectors = fetchSocialConnectors;
|
|
1023
|
+
exports.getCurrentUser = getCurrentUser;
|
|
1024
|
+
exports.getStoredAccessToken = getStoredAccessToken;
|
|
1025
|
+
exports.handleSignInCallback = handleSignInCallback;
|
|
1026
|
+
exports.handleSignInPopupCallback = handleSignInPopupCallback;
|
|
1027
|
+
exports.isIdpConfigured = isIdpConfigured;
|
|
1028
|
+
exports.isOidcPopupWindow = isOidcPopupWindow;
|
|
1029
|
+
exports.prepareSignInRequestUrl = prepareSignInRequestUrl;
|
|
1030
|
+
exports.readIdpConfigFromEnv = readIdpConfigFromEnv;
|
|
1031
|
+
exports.resetUserManager = resetUserManager;
|
|
1032
|
+
exports.resolveExperienceApiBase = resolveExperienceApiBase;
|
|
1033
|
+
exports.resolveIssuer = resolveIssuer;
|
|
1034
|
+
exports.sameOriginAuthorizeUrl = sameOriginAuthorizeUrl;
|
|
1035
|
+
exports.signInPopup = signInPopup;
|
|
1036
|
+
exports.signInRedirect = signInRedirect;
|
|
1037
|
+
exports.signOutRedirect = signOutRedirect;
|
|
1038
|
+
exports.useAuthGate = useAuthGate;
|
|
1039
|
+
exports.useAuthGateOptional = useAuthGateOptional;
|
|
1040
|
+
exports.useLuminaryAuth = useLuminaryAuth;
|
|
1041
|
+
exports.withAuthGateRetry = withAuthGateRetry;
|
|
1042
|
+
//# sourceMappingURL=index.js.map
|
|
1043
|
+
//# sourceMappingURL=index.js.map
|