@iqauth/sdk 2.6.3 → 2.7.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.
Files changed (112) hide show
  1. package/README.md +173 -1
  2. package/dist/browser-session.d.mts +4 -4
  3. package/dist/browser-session.d.ts +4 -4
  4. package/dist/browser-session.js +181 -41
  5. package/dist/browser-session.mjs +3 -3
  6. package/dist/browser.d.mts +5 -5
  7. package/dist/browser.d.ts +5 -5
  8. package/dist/browser.js +271 -32
  9. package/dist/browser.mjs +10 -8
  10. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  11. package/dist/chunk-C2ZTBOAC.mjs +36 -0
  12. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  13. package/dist/chunk-GLXSIGVS.mjs +66 -0
  14. package/dist/{chunk-TKZTCPEK.mjs → chunk-GN37E64I.mjs} +32 -40
  15. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  16. package/dist/{chunk-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
  17. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  18. package/dist/chunk-PMAFENVI.mjs +229 -0
  19. package/dist/chunk-RR2MGPTK.mjs +2724 -0
  20. package/dist/{chunk-76W5TLQQ.mjs → chunk-RTJAIBXY.mjs} +220 -20
  21. package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
  22. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  23. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  24. package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
  25. package/dist/cli/index.js +2 -2
  26. package/dist/cli/index.mjs +2 -2
  27. package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
  28. package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
  29. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  30. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  31. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  32. package/dist/{express-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
  33. package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
  34. package/dist/express.d.mts +7 -6
  35. package/dist/express.d.ts +7 -6
  36. package/dist/express.js +349 -52
  37. package/dist/express.mjs +39 -12
  38. package/dist/fastify.d.mts +2 -0
  39. package/dist/fastify.d.ts +2 -0
  40. package/dist/fastify.js +332 -52
  41. package/dist/fastify.mjs +23 -8
  42. package/dist/hono.d.mts +2 -0
  43. package/dist/hono.d.ts +2 -0
  44. package/dist/hono.js +329 -52
  45. package/dist/hono.mjs +20 -8
  46. package/dist/index-5KSZEnDe.d.ts +1626 -0
  47. package/dist/index-CKoZHAoc.d.mts +1626 -0
  48. package/dist/index.d.mts +56 -8
  49. package/dist/index.d.ts +56 -8
  50. package/dist/index.js +565 -69
  51. package/dist/index.mjs +29 -9
  52. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  53. package/dist/locales.d.mts +1 -1
  54. package/dist/locales.d.ts +1 -1
  55. package/dist/mobile.d.mts +77 -7
  56. package/dist/mobile.d.ts +77 -7
  57. package/dist/mobile.js +276 -41
  58. package/dist/mobile.mjs +98 -3
  59. package/dist/next.d.mts +2 -1
  60. package/dist/next.d.ts +2 -1
  61. package/dist/next.js +391 -201
  62. package/dist/next.mjs +22 -7
  63. package/dist/pkce-7WKV4OIN.mjs +11 -0
  64. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
  65. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
  66. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  67. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  68. package/dist/react-permissions.d.mts +52 -0
  69. package/dist/react-permissions.d.ts +52 -0
  70. package/dist/react-permissions.js +239 -0
  71. package/dist/react-permissions.mjs +97 -0
  72. package/dist/react.d.mts +9 -1624
  73. package/dist/react.d.ts +9 -1624
  74. package/dist/react.js +343 -36
  75. package/dist/react.mjs +59 -2611
  76. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  77. package/dist/server/handlers.d.mts +148 -3
  78. package/dist/server/handlers.d.ts +148 -3
  79. package/dist/server/handlers.js +410 -11
  80. package/dist/server/handlers.mjs +12 -3
  81. package/dist/server.d.mts +151 -8
  82. package/dist/server.d.ts +151 -8
  83. package/dist/server.js +406 -50
  84. package/dist/server.mjs +93 -11
  85. package/dist/service.d.mts +4 -4
  86. package/dist/service.d.ts +4 -4
  87. package/dist/service.js +181 -41
  88. package/dist/service.mjs +3 -3
  89. package/dist/{signIn-CiIBTJIh.d.mts → signIn-BLFnz8SV.d.ts} +78 -3
  90. package/dist/{signIn-CCY4JE5G.mjs → signIn-SHBW6Z4T.mjs} +2 -1
  91. package/dist/{signIn-OCr88Zf8.d.ts → signIn-T-CZ6t6r.d.mts} +78 -3
  92. package/dist/test.mjs +3 -3
  93. package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
  94. package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
  95. package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
  96. package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
  97. package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
  98. package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
  99. package/dist/webhooks.d.mts +100 -17
  100. package/dist/webhooks.d.ts +100 -17
  101. package/dist/webhooks.js +164 -15
  102. package/dist/webhooks.mjs +7 -1
  103. package/dist/ws.d.mts +2 -2
  104. package/dist/ws.d.ts +2 -2
  105. package/dist/ws.js +80 -30
  106. package/dist/ws.mjs +4 -4
  107. package/docs/error-handling.md +101 -0
  108. package/docs/guides/effective-permissions.md +171 -0
  109. package/package.json +13 -3
  110. package/dist/chunk-UKZLOHZG.mjs +0 -83
  111. package/dist/errors-CDdl24MP.d.mts +0 -52
  112. package/dist/errors-CDdl24MP.d.ts +0 -52
@@ -0,0 +1,2724 @@
1
+ import {
2
+ AccountRegistry,
3
+ MultiAccountTokenStore,
4
+ SessionManager,
5
+ defaultCookieStore,
6
+ enrollPasskey,
7
+ linkProvider,
8
+ listLinkedIdentities,
9
+ requestMagicLink,
10
+ signInWithPasskey,
11
+ unlinkProvider
12
+ } from "./chunk-RTJAIBXY.mjs";
13
+ import {
14
+ handleAuthCallback,
15
+ redirectToSignIn,
16
+ signIn,
17
+ signOut
18
+ } from "./chunk-GN37E64I.mjs";
19
+ import {
20
+ defaultBundle,
21
+ localizeErrorCode,
22
+ resolveBundle,
23
+ t
24
+ } from "./chunk-5T7GHBX6.mjs";
25
+
26
+ // src/browser/returnTo.ts
27
+ function normalizeOrigin(o) {
28
+ try {
29
+ return new URL(o).origin;
30
+ } catch {
31
+ return o.replace(/\/+$/, "");
32
+ }
33
+ }
34
+ function sanitizeReturnTo(input, options = {}) {
35
+ const fallback = options.fallback ?? "/";
36
+ if (!input || typeof input !== "string") return fallback;
37
+ const trimmed = input.trim();
38
+ if (!trimmed) return fallback;
39
+ if (trimmed.startsWith("//")) return fallback;
40
+ if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
41
+ return trimmed;
42
+ }
43
+ if (!/^[a-z][a-z0-9+\-.]*:/i.test(trimmed)) {
44
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
45
+ }
46
+ let parsed;
47
+ try {
48
+ parsed = new URL(trimmed);
49
+ } catch {
50
+ return fallback;
51
+ }
52
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return fallback;
53
+ const currentOrigin = options.currentOrigin ?? (typeof window !== "undefined" ? window.location.origin : "");
54
+ const allowed = /* @__PURE__ */ new Set();
55
+ if (currentOrigin) allowed.add(normalizeOrigin(currentOrigin));
56
+ for (const o of options.allowedOrigins ?? []) allowed.add(normalizeOrigin(o));
57
+ if (allowed.has(parsed.origin)) return parsed.toString();
58
+ return fallback;
59
+ }
60
+ function isReturnToAllowed(input, options = {}) {
61
+ const fallback = options.fallback ?? "/";
62
+ const out = sanitizeReturnTo(input, options);
63
+ if (!input) return false;
64
+ return out !== fallback || input === fallback;
65
+ }
66
+
67
+ // src/react/index.tsx
68
+ import {
69
+ createContext,
70
+ createElement,
71
+ Fragment,
72
+ useCallback,
73
+ useContext,
74
+ useEffect,
75
+ useMemo,
76
+ useRef,
77
+ useState,
78
+ useSyncExternalStore
79
+ } from "react";
80
+
81
+ // src/browser/hostedIssuerGuard.ts
82
+ var HOSTED_ISSUER_MISMATCH_CODE = "IQAUTH_HOSTED_ISSUER_MISMATCH";
83
+ var HOSTED_ISSUER_MISMATCH_DOCS_URL = "https://docs.dispositioniq.com/iqauth/errors#hosted-issuer-mismatch";
84
+ function computeHostedIssuerMismatch(input) {
85
+ const {
86
+ nodeEnv,
87
+ fetchError,
88
+ explicitOverride,
89
+ resolvedBaseUrl,
90
+ managerIssuerUrl,
91
+ hostedIssuerUrl,
92
+ appKey
93
+ } = input;
94
+ if (nodeEnv === "production") return null;
95
+ if (!fetchError) return null;
96
+ if (explicitOverride) return null;
97
+ if (!managerIssuerUrl || !hostedIssuerUrl) return null;
98
+ if (resolvedBaseUrl !== managerIssuerUrl) return null;
99
+ if (resolvedBaseUrl === hostedIssuerUrl) return null;
100
+ const e = new Error(
101
+ `[IQAuth] ${HOSTED_ISSUER_MISMATCH_CODE}: <SignIn /> targeted "${resolvedBaseUrl}" (inherited from <IQAuthProvider issuer="\u2026"/>), but that host does not serve /api/public/apps/${appKey}/sign-in-context. The hosted UI lives at the publishable key's issuer: "${hostedIssuerUrl}". Fix: drop the <IQAuthProvider issuer="\u2026"/> override so the SDK uses the publishable key's iss, OR pass <SignIn iqAuthBaseUrl="${hostedIssuerUrl}"/> explicitly. Docs: ${HOSTED_ISSUER_MISMATCH_DOCS_URL}. Underlying fetch error: ${fetchError}`
102
+ );
103
+ e.code = HOSTED_ISSUER_MISMATCH_CODE;
104
+ return e;
105
+ }
106
+
107
+ // src/react/index.tsx
108
+ import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
109
+ var globalManager = null;
110
+ function setGlobalManager(m) {
111
+ globalManager = m;
112
+ }
113
+ function getGlobalManager() {
114
+ return globalManager;
115
+ }
116
+ var IQAuthContext = createContext(null);
117
+ function IQAuthProvider({
118
+ publishableKey,
119
+ issuer,
120
+ channelName,
121
+ proactiveRefresh,
122
+ manager: externalManager,
123
+ allowedReturnOrigins,
124
+ appearance,
125
+ roleMapper,
126
+ cookieNames,
127
+ localization,
128
+ silentSso,
129
+ children
130
+ }) {
131
+ const managerRef = useRef(null);
132
+ if (!managerRef.current) {
133
+ managerRef.current = externalManager ?? new SessionManager({
134
+ publishableKey,
135
+ issuer,
136
+ channelName,
137
+ proactiveRefresh,
138
+ cookieNames
139
+ });
140
+ setGlobalManager(managerRef.current);
141
+ }
142
+ const manager = managerRef.current;
143
+ const subscribe = useCallback(
144
+ (cb) => manager.subscribe(() => cb()),
145
+ [manager]
146
+ );
147
+ const getSnapshot = useCallback(() => manager.getSnapshot(), [manager]);
148
+ const getServerSnapshot = useCallback(() => manager.getSnapshot(), [manager]);
149
+ const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
150
+ useEffect(() => {
151
+ void manager.bootstrap();
152
+ const onVisible = () => {
153
+ if (typeof document !== "undefined" && document.visibilityState === "visible") {
154
+ void manager.refresh();
155
+ }
156
+ };
157
+ if (typeof document !== "undefined") {
158
+ document.addEventListener("visibilitychange", onVisible);
159
+ }
160
+ return () => {
161
+ if (typeof document !== "undefined") {
162
+ document.removeEventListener("visibilitychange", onVisible);
163
+ }
164
+ };
165
+ }, [manager]);
166
+ const resolvedLocalization = useMemo(
167
+ () => resolveBundle(localization),
168
+ [localization]
169
+ );
170
+ const value = useMemo(
171
+ () => ({
172
+ manager,
173
+ snapshot,
174
+ allowedReturnOrigins: allowedReturnOrigins ?? [],
175
+ appearance: appearance ?? null,
176
+ roleMapper: roleMapper ?? null,
177
+ localization: resolvedLocalization,
178
+ silentSso: silentSso ?? false
179
+ }),
180
+ [manager, snapshot, allowedReturnOrigins, appearance, roleMapper, resolvedLocalization, silentSso]
181
+ );
182
+ return createElement(IQAuthContext.Provider, { value }, children);
183
+ }
184
+ function __useIQAuthInternal() {
185
+ return useCtx();
186
+ }
187
+ function useCtx() {
188
+ const ctx = useContext(IQAuthContext);
189
+ if (!ctx) throw new Error("IQAuth hooks must be used inside <IQAuthProvider>");
190
+ return ctx;
191
+ }
192
+ function useLocale() {
193
+ const ctx = useContext(IQAuthContext);
194
+ return ctx?.localization ?? defaultBundle;
195
+ }
196
+ function useT() {
197
+ const bundle = useLocale();
198
+ return useMemo(
199
+ () => (key, vars) => t(bundle, key, vars),
200
+ [bundle]
201
+ );
202
+ }
203
+ function localizeError(bundle, raw) {
204
+ if (!raw) return t(bundle, "errors.generic");
205
+ if (typeof raw === "string") {
206
+ if (/^[A-Z][A-Z0-9_]+$/.test(raw)) return localizeErrorCode(bundle, raw);
207
+ return raw;
208
+ }
209
+ if (raw.code) return localizeErrorCode(bundle, raw.code);
210
+ return raw.message || t(bundle, "errors.generic");
211
+ }
212
+ function useUser() {
213
+ const { snapshot, roleMapper } = useCtx();
214
+ return useMemo(
215
+ () => {
216
+ const user = snapshot.user ? {
217
+ ...snapshot.user,
218
+ // F13 — derive `role` via roleMapper, falling back to a sensible default
219
+ role: (() => {
220
+ if (roleMapper) {
221
+ try {
222
+ return roleMapper(snapshot.claims);
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+ const c = snapshot.claims;
228
+ if (!c) return null;
229
+ if (typeof c.role === "string" && c.role) return c.role;
230
+ if (Array.isArray(c.roles) && c.roles.length > 0 && typeof c.roles[0] === "string") return c.roles[0];
231
+ return null;
232
+ })()
233
+ } : null;
234
+ return {
235
+ isLoaded: snapshot.status !== "loading",
236
+ isSignedIn: snapshot.status === "authenticated" && !!snapshot.user,
237
+ user,
238
+ error: snapshot.error
239
+ };
240
+ },
241
+ [snapshot.status, snapshot.user, snapshot.claims, snapshot.error, snapshot.version, roleMapper]
242
+ );
243
+ }
244
+ function useSession() {
245
+ const { snapshot } = useCtx();
246
+ return useMemo(
247
+ () => ({
248
+ isLoaded: snapshot.status !== "loading",
249
+ isSignedIn: snapshot.status === "authenticated",
250
+ claims: snapshot.claims,
251
+ accessToken: snapshot.accessToken,
252
+ error: snapshot.error
253
+ }),
254
+ [snapshot.status, snapshot.claims, snapshot.accessToken, snapshot.error, snapshot.version]
255
+ );
256
+ }
257
+ function useAuth() {
258
+ const { manager, snapshot } = useCtx();
259
+ return useMemo(
260
+ () => ({
261
+ isLoaded: snapshot.status !== "loading",
262
+ isSignedIn: snapshot.status === "authenticated",
263
+ userId: snapshot.user?.sub ?? null,
264
+ tenantId: snapshot.tenantId,
265
+ error: snapshot.error,
266
+ signIn: (opts) => signIn(manager, opts),
267
+ signOut: (opts) => signOut(manager, opts),
268
+ redirectToSignIn: (opts) => redirectToSignIn(manager, opts),
269
+ getToken: () => manager.getToken(),
270
+ fetch: (input, init) => manager.fetch(input, init)
271
+ }),
272
+ [manager, snapshot.status, snapshot.user, snapshot.tenantId, snapshot.error, snapshot.version]
273
+ );
274
+ }
275
+ function useOrganization() {
276
+ const { snapshot } = useCtx();
277
+ return useMemo(
278
+ () => ({
279
+ isLoaded: snapshot.status !== "loading",
280
+ organization: snapshot.tenantId ? { id: snapshot.tenantId, tenantId: snapshot.tenantId } : null,
281
+ error: snapshot.error
282
+ }),
283
+ [snapshot.status, snapshot.tenantId, snapshot.error, snapshot.version]
284
+ );
285
+ }
286
+ function useAuthFetch() {
287
+ const { manager } = useCtx();
288
+ return useCallback(
289
+ (input, init) => manager.fetch(input, init),
290
+ [manager]
291
+ );
292
+ }
293
+ function useSessionList() {
294
+ const { manager } = useCtx();
295
+ const [sessions, setSessions] = useState([]);
296
+ const [loading, setLoading] = useState(true);
297
+ const [error, setError] = useState(null);
298
+ const base = manager.issuerUrl.replace(/\/$/, "");
299
+ const refresh = useCallback(async () => {
300
+ setLoading(true);
301
+ setError(null);
302
+ try {
303
+ const res = await manager.fetch(`${base}/api/v1/users/me/sessions`);
304
+ const json = await res.json().catch(() => ({}));
305
+ if (!res.ok) throw new Error(json?.error?.message || `HTTP ${res.status}`);
306
+ setSessions(json?.data?.sessions || []);
307
+ } catch (err) {
308
+ setError(err.message);
309
+ } finally {
310
+ setLoading(false);
311
+ }
312
+ }, [manager, base]);
313
+ useEffect(() => {
314
+ void refresh();
315
+ }, [refresh]);
316
+ const revoke = useCallback(async (sessionId) => {
317
+ const res = await manager.fetch(`${base}/api/v1/users/me/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
318
+ if (!res.ok) {
319
+ const j = await res.json().catch(() => ({}));
320
+ throw new Error(j?.error?.message || `HTTP ${res.status}`);
321
+ }
322
+ setSessions((prev) => prev.filter((s) => s.id !== sessionId));
323
+ }, [manager, base]);
324
+ const revokeAllOthers = useCallback(async () => {
325
+ const res = await manager.fetch(`${base}/api/v1/users/me/sessions/revoke-all-others`, { method: "POST" });
326
+ const json = await res.json().catch(() => ({}));
327
+ if (!res.ok) throw new Error(json?.error?.message || `HTTP ${res.status}`);
328
+ setSessions((prev) => prev.filter((s) => s.isCurrent));
329
+ return { terminatedCount: json?.data?.terminatedCount ?? 0 };
330
+ }, [manager, base]);
331
+ return { sessions, loading, error, refresh, revoke, revokeAllOthers };
332
+ }
333
+ async function revokeSession(sessionId) {
334
+ const mgr = getGlobalManager();
335
+ if (!mgr) throw new Error("revokeSession() requires <IQAuthProvider> mounted");
336
+ const base = mgr.issuerUrl.replace(/\/$/, "");
337
+ const res = await mgr.fetch(`${base}/api/v1/users/me/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
338
+ if (!res.ok) {
339
+ const j = await res.json().catch(() => ({}));
340
+ throw new Error(j?.error?.message || `HTTP ${res.status}`);
341
+ }
342
+ }
343
+ var MultisessionContext = createContext(null);
344
+ function MultisessionAppSupport({ children }) {
345
+ const { manager, snapshot } = useCtx();
346
+ const registryRef = useRef(null);
347
+ if (!registryRef.current) {
348
+ registryRef.current = new AccountRegistry(manager.appKey);
349
+ }
350
+ const registry = registryRef.current;
351
+ const [version, setVersion] = useState(0);
352
+ useEffect(() => {
353
+ const store = new MultiAccountTokenStore(registry, defaultCookieStore());
354
+ manager.setTokenStore(store);
355
+ const unsub = registry.subscribe(() => setVersion((v) => v + 1));
356
+ return () => {
357
+ unsub();
358
+ registry.destroy();
359
+ };
360
+ }, [manager]);
361
+ useEffect(() => {
362
+ if (snapshot.status !== "authenticated" || !snapshot.user) return;
363
+ const claims = snapshot.claims;
364
+ const rec = {
365
+ accountId: snapshot.user.sub,
366
+ userId: snapshot.user.sub,
367
+ email: snapshot.user.email,
368
+ name: snapshot.user.name,
369
+ tenantId: snapshot.tenantId ?? claims?.tenantId ?? null,
370
+ addedAt: Date.now()
371
+ };
372
+ registry.upsert(rec);
373
+ if (registry.active() !== rec.accountId) {
374
+ registry.setActive(rec.accountId);
375
+ }
376
+ }, [snapshot.user?.sub, snapshot.tenantId, snapshot.status]);
377
+ const value = useMemo(
378
+ () => ({ registry, version }),
379
+ [registry, version]
380
+ );
381
+ return createElement(MultisessionContext.Provider, { value }, children);
382
+ }
383
+ function useMultiCtx() {
384
+ const ctx = useContext(MultisessionContext);
385
+ if (!ctx) throw new Error("F22 hooks must be used inside <MultisessionAppSupport>");
386
+ return ctx;
387
+ }
388
+ function useAccountList() {
389
+ const { registry, version } = useMultiCtx();
390
+ const { isLoaded } = useUser();
391
+ const accounts = useMemo(() => {
392
+ const active = registry.active();
393
+ return registry.list().map((a) => ({
394
+ accountId: a.accountId,
395
+ userId: a.userId,
396
+ email: a.email,
397
+ name: a.name,
398
+ tenantId: a.tenantId,
399
+ isActive: a.accountId === active
400
+ }));
401
+ }, [registry, version]);
402
+ return { accounts, loading: !isLoaded };
403
+ }
404
+ function useAccountSwitcher() {
405
+ const { manager } = useCtx();
406
+ const { registry } = useMultiCtx();
407
+ const addAccount = useCallback(
408
+ async (opts = {}) => {
409
+ registry.setActive(null);
410
+ await signIn(manager, { ...opts, prompt: "login" });
411
+ },
412
+ [manager, registry]
413
+ );
414
+ const switchTo = useCallback(
415
+ async (accountId) => {
416
+ const target = registry.get(accountId);
417
+ if (!target) throw new Error(`Unknown accountId: ${accountId}`);
418
+ registry.setActive(accountId);
419
+ const ok = await manager.refresh();
420
+ if (!ok) {
421
+ registry.remove(accountId);
422
+ throw new Error("Could not switch account; session may have been revoked");
423
+ }
424
+ },
425
+ [manager, registry]
426
+ );
427
+ const removeAccount = useCallback(
428
+ (accountId) => {
429
+ registry.remove(accountId);
430
+ },
431
+ [registry]
432
+ );
433
+ return { addAccount, switchTo, removeAccount };
434
+ }
435
+ function SignedIn({ children }) {
436
+ const { isSignedIn, isLoaded } = useUser();
437
+ if (!isLoaded || !isSignedIn) return null;
438
+ return createElement(Fragment, null, children);
439
+ }
440
+ function SignedOut({ children }) {
441
+ const { isSignedIn, isLoaded } = useUser();
442
+ if (!isLoaded || isSignedIn) return null;
443
+ return createElement(Fragment, null, children);
444
+ }
445
+ function IQAuthLoading({ children }) {
446
+ const { isLoaded } = useAuth();
447
+ if (isLoaded) return null;
448
+ return createElement(Fragment, null, children);
449
+ }
450
+ function IQAuthLoaded({ children }) {
451
+ const { isLoaded } = useAuth();
452
+ if (!isLoaded) return null;
453
+ return createElement(Fragment, null, children);
454
+ }
455
+ function RedirectToSignIn(props = {}) {
456
+ const { manager, snapshot } = useCtx();
457
+ const [error, setError] = useState(null);
458
+ useEffect(() => {
459
+ if (snapshot.status === "unauthenticated") {
460
+ redirectToSignIn(manager, props).catch((err) => {
461
+ const message = err instanceof Error ? err.message : String(err);
462
+ console.error("[IQAuth] RedirectToSignIn failed:", err, {
463
+ issuer: manager.issuerUrl,
464
+ appKey: manager.appKey
465
+ });
466
+ setError(message);
467
+ });
468
+ }
469
+ }, [manager, snapshot.status]);
470
+ if (error) {
471
+ return createElement(
472
+ "div",
473
+ {
474
+ role: "alert",
475
+ style: {
476
+ maxWidth: 520,
477
+ margin: "48px auto",
478
+ padding: 16,
479
+ border: "1px solid #f0c0c0",
480
+ background: "#fff5f5",
481
+ borderRadius: 8,
482
+ fontFamily: "system-ui, sans-serif",
483
+ fontSize: 14,
484
+ color: "#7a1f1f"
485
+ }
486
+ },
487
+ createElement("strong", null, "IQAuth: sign-in redirect failed"),
488
+ createElement("div", { style: { marginTop: 8, fontSize: 13 } }, error),
489
+ createElement(
490
+ "div",
491
+ { style: { marginTop: 12, fontSize: 12, color: "#5a4a4a" } },
492
+ `Issuer: ${manager.issuerUrl} \xB7 App: ${manager.appKey}. Check browser console for details.`
493
+ )
494
+ );
495
+ }
496
+ return null;
497
+ }
498
+ function asArray(v) {
499
+ if (v == null) return [];
500
+ return Array.isArray(v) ? v : [v];
501
+ }
502
+ function claimRoles(c) {
503
+ if (!c) return [];
504
+ const x = c;
505
+ if (Array.isArray(x.roles)) return x.roles.filter((r) => typeof r === "string");
506
+ if (typeof x.role === "string") return [x.role];
507
+ return [];
508
+ }
509
+ function claimPermissions(c) {
510
+ if (!c) return [];
511
+ const x = c;
512
+ const out = /* @__PURE__ */ new Set();
513
+ if (Array.isArray(x.permissions)) {
514
+ for (const p of x.permissions) if (typeof p === "string") out.add(p);
515
+ }
516
+ if (Array.isArray(x.entitlements)) {
517
+ for (const p of x.entitlements) if (typeof p === "string") out.add(p);
518
+ }
519
+ if (typeof x.scope === "string") {
520
+ for (const s of x.scope.split(/\s+/)) if (s) out.add(s);
521
+ }
522
+ return Array.from(out);
523
+ }
524
+ function Protect({ role, permission, condition, fallback = null, children }) {
525
+ const { snapshot } = useCtx();
526
+ if (snapshot.status !== "authenticated") return createElement(Fragment, null, fallback);
527
+ const wantedRoles = asArray(role);
528
+ const wantedPerms = asArray(permission);
529
+ if (wantedRoles.length) {
530
+ const have = new Set(claimRoles(snapshot.claims));
531
+ if (!wantedRoles.some((r) => have.has(r))) return createElement(Fragment, null, fallback);
532
+ }
533
+ if (wantedPerms.length) {
534
+ const have = new Set(claimPermissions(snapshot.claims));
535
+ if (!wantedPerms.some((p) => have.has(p))) return createElement(Fragment, null, fallback);
536
+ }
537
+ if (condition && !condition(snapshot.claims)) return createElement(Fragment, null, fallback);
538
+ return createElement(Fragment, null, children);
539
+ }
540
+ function RedirectToSignedIn({ to = "/", replace = true } = {}) {
541
+ const { snapshot, allowedReturnOrigins } = useCtx();
542
+ useEffect(() => {
543
+ if (snapshot.status !== "authenticated") return;
544
+ if (typeof window === "undefined") return;
545
+ const safe = sanitizeReturnTo(to, { allowedOrigins: allowedReturnOrigins, fallback: "/" });
546
+ if (replace) window.location.replace(safe);
547
+ else window.location.assign(safe);
548
+ }, [snapshot.status, to, replace, allowedReturnOrigins]);
549
+ return null;
550
+ }
551
+ function useReturnTo(options = {}) {
552
+ const { allowedReturnOrigins } = useCtx();
553
+ const paramName = options.paramName ?? "return_to";
554
+ const storageKey = options.storageKey ?? "iqauth_return_to";
555
+ const fallback = options.fallback ?? "/";
556
+ return useMemo(() => {
557
+ if (typeof window === "undefined") return fallback;
558
+ let raw = null;
559
+ try {
560
+ const params = new URLSearchParams(window.location.search);
561
+ raw = params.get(paramName) || params.get("next");
562
+ } catch {
563
+ }
564
+ if (!raw) {
565
+ try {
566
+ raw = window.sessionStorage.getItem(storageKey);
567
+ } catch {
568
+ }
569
+ }
570
+ const safe = sanitizeReturnTo(raw, { allowedOrigins: allowedReturnOrigins, fallback });
571
+ if (safe !== fallback) {
572
+ try {
573
+ window.sessionStorage.setItem(storageKey, safe);
574
+ } catch {
575
+ }
576
+ }
577
+ return safe;
578
+ }, [paramName, storageKey, fallback, allowedReturnOrigins]);
579
+ }
580
+ function IQAuthReturnToBouncer({ children, ...opts }) {
581
+ const { snapshot } = useCtx();
582
+ const returnTo = useReturnTo(opts);
583
+ useEffect(() => {
584
+ if (snapshot.status !== "authenticated") return;
585
+ if (typeof window === "undefined") return;
586
+ window.location.replace(returnTo);
587
+ }, [snapshot.status, returnTo]);
588
+ if (snapshot.status === "authenticated") return null;
589
+ return createElement(Fragment, null, children);
590
+ }
591
+ async function preflightReturnTo(args) {
592
+ const f = args.fetchImpl ?? fetch;
593
+ const url = `${args.iqAuthBaseUrl.replace(/\/$/, "")}/api/public/apps/${encodeURIComponent(args.appKey)}/sign-in-context?return_to=${encodeURIComponent(args.returnTo)}`;
594
+ try {
595
+ const r = await f(url, { credentials: "include" });
596
+ const body = await r.json().catch(() => null);
597
+ if (!r.ok || !body?.success || !body.data) {
598
+ return { ok: false, allowedOrigins: [], reason: body?.error?.message || `HTTP ${r.status}` };
599
+ }
600
+ return { ok: !!body.data.returnAllowed, allowedOrigins: body.data.allowedOrigins ?? [], reason: body.data.returnAllowed ? void 0 : "returnTo not in app allowedOrigins" };
601
+ } catch (err) {
602
+ return { ok: false, allowedOrigins: [], reason: err.message };
603
+ }
604
+ }
605
+ function AuthCallback({ onComplete, fallback } = {}) {
606
+ const { manager } = useCtx();
607
+ useEffect(() => {
608
+ let cancelled = false;
609
+ void handleAuthCallback(manager).then((result) => {
610
+ if (cancelled) return;
611
+ if (onComplete) onComplete(result);
612
+ else if (typeof window !== "undefined") {
613
+ window.location.replace(result.returnTo || "/");
614
+ }
615
+ });
616
+ return () => {
617
+ cancelled = true;
618
+ };
619
+ }, [manager, onComplete]);
620
+ return createElement(Fragment, null, fallback ?? null);
621
+ }
622
+ function brandStyle(branding) {
623
+ if (!branding) return {};
624
+ const s = {};
625
+ if (branding.primaryColor) s["--brand-primary"] = branding.primaryColor;
626
+ if (branding.accentColor) s["--brand-accent"] = branding.accentColor;
627
+ if (branding.backgroundColor) s["--brand-bg"] = branding.backgroundColor;
628
+ if (branding.surfaceColor) s["--brand-surface"] = branding.surfaceColor;
629
+ if (branding.textColor) s["--brand-text"] = branding.textColor;
630
+ if (branding.borderRadius != null && branding.borderRadius !== "") {
631
+ const n = typeof branding.borderRadius === "number" ? `${branding.borderRadius}px` : String(branding.borderRadius);
632
+ s["--brand-radius"] = n;
633
+ }
634
+ if (branding.fontFamilyBody) s["--brand-font-body"] = branding.fontFamilyBody;
635
+ if (branding.fontFamilyHeading) s["--brand-font-heading"] = branding.fontFamilyHeading;
636
+ return s;
637
+ }
638
+ async function jsonFetch(url, init) {
639
+ const res = await fetch(url, { ...init, credentials: init?.credentials || "include" });
640
+ const payload = await res.json().catch(() => ({}));
641
+ if (!res.ok && payload?.success !== true) {
642
+ const message = payload?.error?.message || payload?.error_description || payload?.error || `HTTP ${res.status}`;
643
+ throw new Error(typeof message === "string" ? message : "Request failed");
644
+ }
645
+ return payload;
646
+ }
647
+ function useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo) {
648
+ const [ctx, setCtx] = useState(null);
649
+ const [loading, setLoading] = useState(true);
650
+ const [error, setError] = useState(null);
651
+ useEffect(() => {
652
+ if (!appKey) {
653
+ setLoading(false);
654
+ setError("appKey is required");
655
+ return;
656
+ }
657
+ let cancelled = false;
658
+ setLoading(true);
659
+ const url = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/public/apps/${encodeURIComponent(appKey)}/sign-in-context?return_to=${encodeURIComponent(returnTo)}`;
660
+ fetch(url, { credentials: "include" }).then(async (r) => {
661
+ const contentType = r.headers.get("content-type") || "";
662
+ if (!r.ok || !contentType.includes("json")) {
663
+ const bodyPreview = await r.text().then((t2) => t2.slice(0, 160)).catch(() => "");
664
+ console.error(
665
+ `[IQAuth] sign-in-context request failed: ${r.status} ${r.statusText} (content-type: ${contentType || "\u2014"}). URL: ${url}. Common causes: (1) iqAuthBaseUrl points at the wrong host (it should be your IQAuth issuer, e.g. https://auth.dispositioniq.com \u2014 NOT your own app's domain); (2) the app key "${appKey}" is wrong or revoked; (3) CORS preflight is blocked because this origin isn't in the app's allowed-origins list. Response body preview: ${bodyPreview}`
666
+ );
667
+ throw new Error(
668
+ r.status >= 500 ? "Failed to load sign-in context (server error)" : `Failed to load sign-in context (HTTP ${r.status})`
669
+ );
670
+ }
671
+ return r.json();
672
+ }).then((payload) => {
673
+ if (cancelled) return;
674
+ if (payload?.success === false) throw new Error(payload?.error?.message || "Failed to load sign-in context");
675
+ setCtx(payload.data);
676
+ }).catch((err) => {
677
+ if (!cancelled) setError(err.message);
678
+ }).finally(() => {
679
+ if (!cancelled) setLoading(false);
680
+ });
681
+ return () => {
682
+ cancelled = true;
683
+ };
684
+ }, [iqAuthBaseUrl, appKey, returnTo]);
685
+ return { ctx, loading, error };
686
+ }
687
+ var SHELL_CSS = `
688
+ .iqauth-sdk-shell {
689
+ min-height: 100vh;
690
+ width: 100%;
691
+ display: grid;
692
+ grid-template-columns: 1fr;
693
+ background: var(--brand-bg, #f7f7f6);
694
+ color: var(--brand-text, #0f172a);
695
+ /* Container queries so the two-pane layout responds to the SDK's
696
+ RENDERED width, not the viewport. This keeps the form usable when
697
+ a host app embeds <SignIn/> inside a narrower card or sidebar
698
+ instead of full-screen \u2014 the previous viewport @media query would
699
+ happily render the side-by-side hero+form into a 200px container
700
+ and wrap text one character per line. */
701
+ container-type: inline-size;
702
+ container-name: iqauth-sdk;
703
+ }
704
+ .iqauth-sdk-hero { display: none; }
705
+ .iqauth-sdk-pane {
706
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
707
+ padding: 32px 20px;
708
+ /* 2.6.1 \u2014 Default to natural height so embedded usage (inside a card,
709
+ modal, or sidebar) sizes to its content instead of forcing a full
710
+ viewport. The hosted/full-page layout re-introduces 100vh below the
711
+ 768px container-query threshold for the side-by-side hero variant. */
712
+ min-height: auto;
713
+ box-sizing: border-box;
714
+ }
715
+ .iqauth-sdk-card {
716
+ width: 100%; max-width: 480px;
717
+ background: var(--brand-surface, #ffffff);
718
+ border: 1px solid rgba(15,23,42,0.08);
719
+ border-radius: var(--brand-radius, 16px);
720
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 12px 32px rgba(15,23,42,0.06);
721
+ overflow: hidden;
722
+ }
723
+ .iqauth-sdk-shell, .iqauth-sdk-shell input, .iqauth-sdk-shell button { font-family: var(--brand-font-body, inherit); }
724
+ .iqauth-sdk-shell h1, .iqauth-sdk-shell h2, .iqauth-sdk-shell h3 { font-family: var(--brand-font-heading, var(--brand-font-body, inherit)); }
725
+ .iqauth-sdk-shell[data-layout="centered_card"] { grid-template-columns: 1fr; }
726
+ .iqauth-sdk-shell[data-layout="centered_card"] .iqauth-sdk-hero { display: none; }
727
+ .iqauth-sdk-shell[data-layout="full_bleed"] { background-size: cover; background-position: center; background-repeat: no-repeat; }
728
+ .iqauth-sdk-shell[data-layout="full_bleed"] .iqauth-sdk-hero { display: none; }
729
+ .iqauth-sdk-shell[data-layout="full_bleed"] .iqauth-sdk-pane { background: rgba(15,23,42,0.30); }
730
+ .iqauth-sdk-shell[data-social-style="outline"] .iqauth-sdk-google-btn { background: transparent; border-color: var(--brand-primary, rgba(15,23,42,0.18)); color: var(--brand-text, inherit); }
731
+ .iqauth-sdk-shell[data-social-style="ghost"] .iqauth-sdk-google-btn { background: transparent; border-color: transparent; color: var(--brand-text, inherit); }
732
+ .iqauth-sdk-shell[data-social-style="solid"] .iqauth-sdk-google-btn { background: var(--brand-primary, #3b82f6); border-color: transparent; color: #fff; }
733
+ .iqauth-sdk-shell[data-social-style="solid"] .iqauth-sdk-google-btn:hover { background: var(--brand-primary, #3b82f6); opacity: 0.92; }
734
+ .iqauth-sdk-card-header { padding: 32px 36px 0; }
735
+ .iqauth-sdk-card-header h1 { font-size: 24px; font-weight: 600; margin: 0; line-height: 1.2; letter-spacing: -0.01em; }
736
+ .iqauth-sdk-card-header p { margin: 8px 0 0; font-size: 14px; color: rgba(15,23,42,0.65); line-height: 1.5; }
737
+ .iqauth-sdk-card-body { padding: 32px 36px 28px; display: flex; flex-direction: column; gap: 18px; }
738
+ .iqauth-sdk-mobile-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }
739
+ .iqauth-sdk-mobile-brand img { height: 36px; width: auto; }
740
+ .iqauth-sdk-mobile-brand span { font-size: 16px; font-weight: 600; }
741
+ .iqauth-sdk-footer { margin-top: 20px; text-align: center; font-size: 13px; color: rgba(15,23,42,0.55); display: flex; flex-direction: column; gap: 8px; align-items: center; }
742
+ .iqauth-sdk-footer-links { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; }
743
+ .iqauth-sdk-footer-links a { color: inherit; opacity: 0.75; text-decoration: none; }
744
+ .iqauth-sdk-footer-links a:hover { opacity: 1; text-decoration: underline; }
745
+ .iqauth-sdk-divider { display: flex; align-items: center; gap: 10px; font-size: 12px; color: rgba(15,23,42,0.45); text-transform: uppercase; letter-spacing: 0.08em; }
746
+ .iqauth-sdk-divider::before, .iqauth-sdk-divider::after { content: ""; flex: 1; height: 1px; background: rgba(15,23,42,0.1); }
747
+ .iqauth-sdk-google-btn {
748
+ display: inline-flex; align-items: center; justify-content: center; gap: 10px;
749
+ width: 100%; padding: 10px 16px; border-radius: 8px;
750
+ background: #fff; color: #0f172a;
751
+ border: 1px solid rgba(15,23,42,0.18);
752
+ font-size: 14px; font-weight: 500; cursor: pointer;
753
+ transition: background 120ms ease, border-color 120ms ease;
754
+ }
755
+ .iqauth-sdk-google-btn:hover { background: #f8fafc; border-color: rgba(15,23,42,0.28); }
756
+ .iqauth-sdk-google-btn[disabled] { opacity: 0.6; cursor: not-allowed; }
757
+
758
+ @container iqauth-sdk (min-width: 768px) {
759
+ .iqauth-sdk-shell:not([data-layout="centered_card"]):not([data-layout="full_bleed"]) { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
760
+ /* 2.6.1 \u2014 Restore 100vh ONLY for the wide side-by-side layout where
761
+ hero+pane need height parity. Embedded narrow uses keep natural height. */
762
+ .iqauth-sdk-pane { padding: 48px 24px; min-height: 100vh; }
763
+ /* 2.6.2 \u2014 Let the card breathe on desktop. The 480px mobile cap looks
764
+ phone-sized on a 720px+ pane; bump to 540px and grow the inner padding
765
+ so the form fills its half of the split layout. */
766
+ .iqauth-sdk-card { max-width: 540px; }
767
+ .iqauth-sdk-card-header { padding: 40px 44px 0; }
768
+ .iqauth-sdk-card-body { padding: 32px 44px 32px; }
769
+ .iqauth-sdk-card-header h1 { font-size: 26px; }
770
+ .iqauth-sdk-hero {
771
+ display: flex; flex-direction: column; justify-content: space-between;
772
+ padding: clamp(32px, 4vw, 56px); color: #ffffff;
773
+ background: linear-gradient(135deg, var(--brand-primary, #3b82f6) 0%, var(--brand-accent, #6366f1) 100%);
774
+ background-size: cover; background-position: center;
775
+ position: relative; overflow: hidden; min-height: 100vh;
776
+ }
777
+ .iqauth-sdk-hero[data-bg-image="true"] {
778
+ background-image: var(--iqauth-sdk-hero-image), linear-gradient(135deg, var(--brand-primary, #3b82f6), var(--brand-accent, #6366f1));
779
+ background-blend-mode: multiply;
780
+ }
781
+ .iqauth-sdk-hero-brand img { height: 40px; width: auto; filter: brightness(0) invert(1); opacity: 0.96; }
782
+ .iqauth-sdk-hero-brand .iqauth-sdk-hero-name { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; }
783
+ .iqauth-sdk-hero-content { max-width: 520px; }
784
+ .iqauth-sdk-hero-content h2 { font-size: clamp(24px, 2.4vw, 34px); font-weight: 600; line-height: 1.2; margin: 0 0 14px; letter-spacing: -0.015em; word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; }
785
+ .iqauth-sdk-hero-content p { font-size: 15px; line-height: 1.6; opacity: 0.92; margin: 0; white-space: pre-wrap; }
786
+ .iqauth-sdk-hero-foot { font-size: 12px; opacity: 0.7; }
787
+ .iqauth-sdk-mobile-brand { display: none; }
788
+ }
789
+ @container iqauth-sdk (min-width: 1280px) {
790
+ .iqauth-sdk-shell:not([data-layout="centered_card"]):not([data-layout="full_bleed"]) { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); }
791
+ /* 2.6.2 \u2014 Extra-wide screens: card grows once more so the form keeps
792
+ pace with the hero pane on 1440px+ monitors. */
793
+ .iqauth-sdk-card { max-width: 580px; }
794
+ }
795
+ `;
796
+ var sdkShellStylesInjected = false;
797
+ var SDK_CSS_MAX_LEN = 50 * 1024;
798
+ var SDK_CSS_FORBIDDEN = [
799
+ /<\/?\s*style[^>]*>/gi,
800
+ /<\/?\s*script[^>]*>/gi,
801
+ /<!--[\s\S]*?-->/g,
802
+ /@import\s+[^;]*;?/gi,
803
+ /expression\s*\(/gi,
804
+ /behavior\s*:/gi,
805
+ /-moz-binding\s*:/gi,
806
+ /javascript\s*:/gi,
807
+ /vbscript\s*:/gi
808
+ ];
809
+ var SDK_URL_DATA_RE = /url\s*\(\s*(['"]?)\s*data\s*:[^)]*\)/gi;
810
+ function sanitizeBrandCss(input) {
811
+ if (!input) return "";
812
+ let out = String(input);
813
+ if (out.length > SDK_CSS_MAX_LEN) out = out.slice(0, SDK_CSS_MAX_LEN);
814
+ out = out.replace(/</g, "");
815
+ for (const re of SDK_CSS_FORBIDDEN) out = out.replace(re, "");
816
+ out = out.replace(SDK_URL_DATA_RE, "url()");
817
+ return out;
818
+ }
819
+ function SdkBrandLogo({ branding, alt, fallback }) {
820
+ const light = branding?.logoLightUrl || branding?.logoUrl || null;
821
+ const dark = branding?.logoDarkUrl || null;
822
+ if (!light && !dark) return /* @__PURE__ */ jsx(Fragment2, { children: fallback });
823
+ const fallbackSrc = light || dark;
824
+ return /* @__PURE__ */ jsxs("picture", { "data-iqauth-sdk-logo": "", children: [
825
+ dark ? /* @__PURE__ */ jsx("source", { srcSet: dark, media: "(prefers-color-scheme: dark)" }) : null,
826
+ light ? /* @__PURE__ */ jsx("source", { srcSet: light, media: "(prefers-color-scheme: light)" }) : null,
827
+ /* @__PURE__ */ jsx("img", { src: fallbackSrc, alt })
828
+ ] });
829
+ }
830
+ var sdkBrandingCache = /* @__PURE__ */ new Map();
831
+ var SDK_BRANDING_TTL_MS = 6e4;
832
+ function flattenBrandingPayload(data) {
833
+ const e = data && data.effective || {};
834
+ const meta = data && data.meta || {};
835
+ return {
836
+ brandName: e.brandName ?? data?.brandName ?? null,
837
+ logoUrl: e.logoLightUrl ?? data?.logoUrl ?? null,
838
+ logoLightUrl: e.logoLightUrl ?? data?.logoLightUrl ?? null,
839
+ logoDarkUrl: e.logoDarkUrl ?? data?.logoDarkUrl ?? null,
840
+ faviconUrl: e.faviconUrl ?? data?.faviconUrl ?? null,
841
+ primaryColor: e.primaryColor ?? data?.primaryColor ?? null,
842
+ accentColor: e.accentColor ?? data?.accentColor ?? null,
843
+ backgroundColor: e.backgroundColor ?? data?.backgroundColor ?? null,
844
+ surfaceColor: e.surfaceColor ?? data?.surfaceColor ?? null,
845
+ textColor: e.textColor ?? data?.textColor ?? null,
846
+ fontFamilyBody: e.fontFamilyBody ?? data?.fontFamilyBody ?? null,
847
+ fontFamilyHeading: e.fontFamilyHeading ?? data?.fontFamilyHeading ?? null,
848
+ customFontUrl: e.customFontUrl ?? data?.customFontUrl ?? null,
849
+ borderRadius: e.borderRadius ?? data?.borderRadius ?? null,
850
+ backgroundImageUrl: e.backgroundImageUrl ?? data?.backgroundImageUrl ?? null,
851
+ customCss: e.customCss ?? data?.customCss ?? null,
852
+ loginLayout: e.loginLayout ?? data?.loginLayout ?? null,
853
+ socialButtonStyle: e.socialButtonStyle ?? data?.socialButtonStyle ?? null,
854
+ footerText: e.footerText ?? data?.footerText ?? null,
855
+ brandingRev: meta.brandingRev ?? data?.brandingRev ?? null
856
+ };
857
+ }
858
+ function useResolvedSdkBranding(iqAuthBaseUrl, appId) {
859
+ const ctx = useContext(IQAuthContext);
860
+ const resolvedAppId = appId ?? ctx?.manager?.appKey ?? null;
861
+ const url = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/public/branding${resolvedAppId ? `?appId=${encodeURIComponent(resolvedAppId)}` : ""}`;
862
+ const cached = sdkBrandingCache.get(url);
863
+ const fresh = cached && Date.now() - cached.ts < SDK_BRANDING_TTL_MS ? cached.data : null;
864
+ const [b, setB] = useState(fresh);
865
+ useEffect(() => {
866
+ let cancelled = false;
867
+ const entry = sdkBrandingCache.get(url);
868
+ const headers = {};
869
+ if (entry?.rev) headers["If-None-Match"] = `W/"brand-${entry.rev}"`;
870
+ if (entry) setB(entry.data);
871
+ fetch(url, { credentials: "include", headers }).then(async (r) => {
872
+ if (cancelled) return;
873
+ if (r.status === 304 && entry) {
874
+ sdkBrandingCache.set(url, { ...entry, ts: Date.now() });
875
+ return;
876
+ }
877
+ if (!r.ok) return;
878
+ const p = await r.json().catch(() => null);
879
+ if (!p?.data) return;
880
+ const flat = flattenBrandingPayload(p.data);
881
+ sdkBrandingCache.set(url, { ts: Date.now(), rev: flat.brandingRev || "", data: flat });
882
+ setB(flat);
883
+ }).catch(() => {
884
+ });
885
+ return () => {
886
+ cancelled = true;
887
+ };
888
+ }, [url]);
889
+ return b;
890
+ }
891
+ function ensureSdkShellStyles() {
892
+ if (typeof document === "undefined" || sdkShellStylesInjected) return;
893
+ const tag = document.createElement("style");
894
+ tag.setAttribute("data-iqauth-sdk-shell", "");
895
+ tag.textContent = SHELL_CSS;
896
+ document.head.appendChild(tag);
897
+ sdkShellStylesInjected = true;
898
+ }
899
+ function useDocumentBranding(branding, fallbackTitle) {
900
+ useEffect(() => {
901
+ if (typeof document === "undefined") return;
902
+ const prevTitle = document.title;
903
+ const brandName = branding?.brandName || "IQAuth";
904
+ document.title = `${fallbackTitle} \xB7 ${brandName}`;
905
+ let linkEl = null;
906
+ let prevHref = null;
907
+ if (branding?.faviconUrl) {
908
+ linkEl = document.querySelector("link[rel='icon']");
909
+ if (!linkEl) {
910
+ linkEl = document.createElement("link");
911
+ linkEl.rel = "icon";
912
+ document.head.appendChild(linkEl);
913
+ } else {
914
+ prevHref = linkEl.href;
915
+ }
916
+ linkEl.href = branding.faviconUrl;
917
+ }
918
+ return () => {
919
+ document.title = prevTitle;
920
+ if (linkEl && prevHref !== null) linkEl.href = prevHref;
921
+ };
922
+ }, [branding?.brandName, branding?.faviconUrl, fallbackTitle]);
923
+ }
924
+ function Shell({
925
+ branding,
926
+ className,
927
+ children,
928
+ title,
929
+ subtitle,
930
+ appearance
931
+ }) {
932
+ ensureSdkShellStyles();
933
+ const t2 = useT();
934
+ useDocumentBranding(branding, title || t2("signIn.title"));
935
+ const brandVars = brandStyle(branding);
936
+ const brandName = branding?.brandName || "IQAuth";
937
+ const heroImage = branding?.heroImageUrl || null;
938
+ const layout = (branding?.loginLayout || "split_screen").toString();
939
+ const bgImage = branding?.backgroundImageUrl || null;
940
+ const fontUrl = branding?.customFontUrl || null;
941
+ const socialStyle = (branding?.socialButtonStyle || "").toString();
942
+ const heroStyle = heroImage ? { ["--iqauth-sdk-hero-image"]: `url("${heroImage.replace(/"/g, '\\"')}")` } : {};
943
+ const shellStyle = {
944
+ ...brandVars,
945
+ ...layout === "full_bleed" && bgImage ? { backgroundImage: `linear-gradient(rgba(15,23,42,0.35), rgba(15,23,42,0.45)), url("${bgImage.replace(/"/g, '\\"')}")` } : {},
946
+ ...appearance?.elements?.rootBox?.style || {}
947
+ };
948
+ const ap = appearance?.elements;
949
+ const supportLink = branding?.supportUrl ? branding.supportUrl : branding?.supportEmail ? `mailto:${branding.supportEmail}` : null;
950
+ const hasFooterLinks = !!(branding?.termsUrl || branding?.privacyUrl || supportLink);
951
+ return /* @__PURE__ */ jsxs(
952
+ "div",
953
+ {
954
+ className: `iqauth-sdk-shell${className ? ` ${className}` : ""}${ap?.rootBox?.className ? ` ${ap.rootBox.className}` : ""}`,
955
+ "data-layout": layout,
956
+ "data-social-style": socialStyle || void 0,
957
+ style: shellStyle,
958
+ "data-iqauth-shell": "",
959
+ "data-branding-rev": branding?.brandingRev || "",
960
+ children: [
961
+ /* @__PURE__ */ jsx("style", { "data-brand": true, children: [
962
+ fontUrl ? `@font-face{font-family:"${(branding?.fontFamilyBody || "Brand").replace(/"/g, "")}";src:url("${fontUrl.replace(/"/g, '\\"')}");font-display:swap;}` : "",
963
+ sanitizeBrandCss(branding?.customCss)
964
+ ].filter(Boolean).join("\n") }),
965
+ /* @__PURE__ */ jsxs("aside", { className: "iqauth-sdk-hero", "data-bg-image": heroImage ? "true" : "false", style: heroStyle, "aria-hidden": "true", children: [
966
+ /* @__PURE__ */ jsx("div", { className: "iqauth-sdk-hero-brand", style: { display: "flex", alignItems: "center", gap: 12 }, children: /* @__PURE__ */ jsx(SdkBrandLogo, { branding, alt: "", fallback: /* @__PURE__ */ jsx("span", { className: "iqauth-sdk-hero-name", children: brandName }) }) }),
967
+ /* @__PURE__ */ jsxs("div", { className: "iqauth-sdk-hero-content", children: [
968
+ /* @__PURE__ */ jsx("h2", { children: branding?.tagline || `Welcome to ${brandName}` }),
969
+ /* @__PURE__ */ jsx("p", { children: branding?.loginSideCopy || branding?.loginSubheadline || `Sign in to continue to your ${brandName} workspace.` })
970
+ ] }),
971
+ /* @__PURE__ */ jsx("div", { className: "iqauth-sdk-hero-foot", children: `\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${brandName}` })
972
+ ] }),
973
+ /* @__PURE__ */ jsx("div", { className: "iqauth-sdk-pane", children: /* @__PURE__ */ jsxs("main", { style: { width: "100%", maxWidth: 420 }, children: [
974
+ /* @__PURE__ */ jsx("div", { className: "iqauth-sdk-mobile-brand", children: /* @__PURE__ */ jsx(SdkBrandLogo, { branding, alt: `${brandName} logo`, fallback: /* @__PURE__ */ jsx("span", { children: brandName }) }) }),
975
+ /* @__PURE__ */ jsxs(
976
+ "section",
977
+ {
978
+ className: `iqauth-sdk-card${ap?.card?.className ? ` ${ap.card.className}` : ""}`,
979
+ style: ap?.card?.style,
980
+ children: [
981
+ title || subtitle ? /* @__PURE__ */ jsxs(
982
+ "div",
983
+ {
984
+ className: `iqauth-sdk-card-header${ap?.cardHeader?.className ? ` ${ap.cardHeader.className}` : ""}`,
985
+ style: ap?.cardHeader?.style,
986
+ children: [
987
+ title ? /* @__PURE__ */ jsx("h1", { className: ap?.headerTitle?.className, style: ap?.headerTitle?.style, children: title }) : null,
988
+ subtitle ? /* @__PURE__ */ jsx("p", { className: ap?.headerSubtitle?.className, style: ap?.headerSubtitle?.style, children: subtitle }) : null
989
+ ]
990
+ }
991
+ ) : null,
992
+ /* @__PURE__ */ jsx(
993
+ "div",
994
+ {
995
+ className: `iqauth-sdk-card-body${ap?.cardBody?.className ? ` ${ap.cardBody.className}` : ""}`,
996
+ style: ap?.cardBody?.style,
997
+ children
998
+ }
999
+ )
1000
+ ]
1001
+ }
1002
+ ),
1003
+ hasFooterLinks || branding?.footerText ? /* @__PURE__ */ jsxs("footer", { className: "iqauth-sdk-footer", children: [
1004
+ branding?.footerText ? /* @__PURE__ */ jsx("div", { "data-testid": "text-brand-footer-sdk", style: { fontSize: 12, opacity: 0.75 }, children: branding.footerText }) : null,
1005
+ hasFooterLinks ? /* @__PURE__ */ jsxs("div", { className: "iqauth-sdk-footer-links", children: [
1006
+ branding?.termsUrl ? /* @__PURE__ */ jsx("a", { href: branding.termsUrl, target: "_blank", rel: "noreferrer noopener", children: "Terms" }) : null,
1007
+ branding?.privacyUrl ? /* @__PURE__ */ jsx("a", { href: branding.privacyUrl, target: "_blank", rel: "noreferrer noopener", children: "Privacy" }) : null,
1008
+ supportLink ? /* @__PURE__ */ jsx("a", { href: supportLink, target: "_blank", rel: "noreferrer noopener", children: "Support" }) : null
1009
+ ] }) : null
1010
+ ] }) : null
1011
+ ] }) })
1012
+ ]
1013
+ },
1014
+ branding?.brandingRev || void 0
1015
+ );
1016
+ }
1017
+ function Field({ label, children }) {
1018
+ return /* @__PURE__ */ jsxs("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: 13 }, children: [
1019
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 500 }, children: label }),
1020
+ children
1021
+ ] });
1022
+ }
1023
+ function inputStyle() {
1024
+ return {
1025
+ padding: "8px 12px",
1026
+ border: "1px solid rgba(15,23,42,0.15)",
1027
+ borderRadius: 6,
1028
+ fontSize: 14,
1029
+ width: "100%",
1030
+ background: "transparent",
1031
+ color: "inherit"
1032
+ };
1033
+ }
1034
+ function PrimaryButton(props) {
1035
+ return /* @__PURE__ */ jsx(
1036
+ "button",
1037
+ {
1038
+ ...props,
1039
+ style: {
1040
+ background: "var(--brand-primary, #3b82f6)",
1041
+ color: "#fff",
1042
+ border: "none",
1043
+ padding: "10px 16px",
1044
+ borderRadius: 8,
1045
+ fontSize: 14,
1046
+ fontWeight: 500,
1047
+ cursor: props.disabled ? "not-allowed" : "pointer",
1048
+ opacity: props.disabled ? 0.6 : 1,
1049
+ width: "100%",
1050
+ ...props.style
1051
+ }
1052
+ }
1053
+ );
1054
+ }
1055
+ function GhostButton(props) {
1056
+ return /* @__PURE__ */ jsx(
1057
+ "button",
1058
+ {
1059
+ ...props,
1060
+ style: {
1061
+ background: "transparent",
1062
+ color: "inherit",
1063
+ border: "1px solid rgba(15,23,42,0.15)",
1064
+ padding: "10px 16px",
1065
+ borderRadius: 8,
1066
+ fontSize: 14,
1067
+ fontWeight: 500,
1068
+ cursor: props.disabled ? "not-allowed" : "pointer",
1069
+ width: "100%",
1070
+ ...props.style
1071
+ }
1072
+ }
1073
+ );
1074
+ }
1075
+ function ErrorBanner({ message }) {
1076
+ return /* @__PURE__ */ jsx("div", { role: "alert", style: {
1077
+ borderLeft: "3px solid #dc2626",
1078
+ background: "rgba(220,38,38,0.08)",
1079
+ padding: "10px 14px",
1080
+ borderRadius: "0 6px 6px 0",
1081
+ fontSize: 13,
1082
+ color: "#b91c1c"
1083
+ }, children: message });
1084
+ }
1085
+ function isSilentSsoEligible(ctx, effectivePrompt) {
1086
+ if (!ctx) return false;
1087
+ if (effectivePrompt === "login") return false;
1088
+ if (!ctx.session) return false;
1089
+ if (!ctx.app.defaultClientId) return false;
1090
+ if (!ctx.returnAllowed) return false;
1091
+ return true;
1092
+ }
1093
+ function SignIn(props) {
1094
+ const providerCtx = useContext(IQAuthContext);
1095
+ const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.hostedIssuerUrl ?? "";
1096
+ const appKey = props.appKey ?? providerCtx?.manager.appKey ?? "";
1097
+ const returnTo = props.returnTo ?? (typeof window !== "undefined" ? `${window.location.origin}/api/iqauth/callback` : "");
1098
+ const { onRedirect, className, prompt, appearance: instanceAppearance, silentSso: instanceSilentSso } = props;
1099
+ const silentSsoEnabled = instanceSilentSso ?? providerCtx?.silentSso ?? false;
1100
+ const appearance = instanceAppearance && providerCtx?.appearance ? { elements: { ...providerCtx.appearance.elements, ...instanceAppearance.elements } } : instanceAppearance ?? providerCtx?.appearance ?? null;
1101
+ if (!iqAuthBaseUrl || !appKey) {
1102
+ console.error(
1103
+ "[IQAuth] <SignIn /> could not determine iqAuthBaseUrl/appKey. Either pass them explicitly OR wrap the component in <IQAuthProvider publishableKey=\u2026/>."
1104
+ );
1105
+ }
1106
+ const t2 = useT();
1107
+ const localeBundle = useLocale();
1108
+ const { ctx, loading, error } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo);
1109
+ const guardError = computeHostedIssuerMismatch({
1110
+ nodeEnv: typeof process !== "undefined" ? process.env?.NODE_ENV : void 0,
1111
+ fetchError: error,
1112
+ explicitOverride: !!props.iqAuthBaseUrl,
1113
+ resolvedBaseUrl: iqAuthBaseUrl,
1114
+ managerIssuerUrl: providerCtx?.manager.issuerUrl,
1115
+ hostedIssuerUrl: providerCtx?.manager.hostedIssuerUrl,
1116
+ appKey
1117
+ });
1118
+ if (guardError) throw guardError;
1119
+ const preflightLoggedRef = useRef(false);
1120
+ useEffect(() => {
1121
+ if (!ctx || preflightLoggedRef.current) return;
1122
+ if (ctx.returnAllowed) return;
1123
+ preflightLoggedRef.current = true;
1124
+ console.error(
1125
+ `[IQAuth] returnTo "${returnTo}" is NOT in the app's allowed origins. Add it via the IQAuth admin console: Apps \u2192 ${ctx.app.key} \u2192 Allowed Origins. Currently allowed: [${ctx.allowedOrigins.join(", ") || "\u2014"}].`
1126
+ );
1127
+ }, [ctx, returnTo]);
1128
+ const [email, setEmail] = useState("");
1129
+ const [password, setPassword] = useState("");
1130
+ const [submitting, setSubmitting] = useState(false);
1131
+ const [formError, setFormError] = useState("");
1132
+ const [mfa, setMfa] = useState(null);
1133
+ const [tenantSel, setTenantSel] = useState(null);
1134
+ const [oauthExchanging, setOauthExchanging] = useState(false);
1135
+ const [silent, setSilent] = useState("idle");
1136
+ const [forcePrompt, setForcePrompt] = useState(false);
1137
+ const effectivePrompt = useMemo(() => {
1138
+ if (prompt === "login" || forcePrompt) return "login";
1139
+ if (!silentSsoEnabled) return "login";
1140
+ if (typeof window !== "undefined") {
1141
+ try {
1142
+ if (new URLSearchParams(window.location.search).get("prompt") === "login") return "login";
1143
+ } catch {
1144
+ }
1145
+ }
1146
+ return void 0;
1147
+ }, [prompt, forcePrompt, silentSsoEnabled]);
1148
+ const oidcPayload = () => ({
1149
+ client_id: ctx?.app.defaultClientId,
1150
+ redirect_uri: returnTo,
1151
+ scope: "openid"
1152
+ });
1153
+ const handlePayload = (payload) => {
1154
+ if (payload.type === "redirect" && payload.redirectUrl) {
1155
+ (onRedirect || ((u) => {
1156
+ window.location.href = u;
1157
+ }))(payload.redirectUrl);
1158
+ return true;
1159
+ }
1160
+ if (payload.type === "tenant_selection") {
1161
+ setTenantSel({ token: payload.tenantSelectionToken, tenants: payload.tenants || [] });
1162
+ return true;
1163
+ }
1164
+ if (payload.type === "mfa_required") {
1165
+ const methods = payload.availableMethods || ["totp"];
1166
+ setMfa({ token: payload.mfaChallengeToken, methods, selected: methods[0], code: "", backup: false });
1167
+ return true;
1168
+ }
1169
+ return false;
1170
+ };
1171
+ const submitLogin = async (e) => {
1172
+ e.preventDefault();
1173
+ if (!ctx?.app.defaultClientId) {
1174
+ setFormError("Application is not configured for hosted sign-in.");
1175
+ return;
1176
+ }
1177
+ setSubmitting(true);
1178
+ setFormError("");
1179
+ try {
1180
+ const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-login`, {
1181
+ method: "POST",
1182
+ headers: { "Content-Type": "application/json" },
1183
+ credentials: "include",
1184
+ body: JSON.stringify({ email, password, ...oidcPayload() })
1185
+ });
1186
+ const payload = await r.json().catch(() => ({}));
1187
+ if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
1188
+ } catch (err) {
1189
+ setFormError(err.message || t(localeBundle, "errors.network"));
1190
+ }
1191
+ setSubmitting(false);
1192
+ };
1193
+ const submitMfa = async (e) => {
1194
+ e.preventDefault();
1195
+ if (!mfa) return;
1196
+ setSubmitting(true);
1197
+ setFormError("");
1198
+ const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-mfa-complete`, {
1199
+ method: "POST",
1200
+ headers: { "Content-Type": "application/json" },
1201
+ credentials: "include",
1202
+ body: JSON.stringify({
1203
+ mfaChallengeToken: mfa.token,
1204
+ code: mfa.code,
1205
+ method: mfa.selected,
1206
+ useBackup: mfa.backup,
1207
+ ...oidcPayload()
1208
+ })
1209
+ });
1210
+ const payload = await r.json().catch(() => ({}));
1211
+ if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
1212
+ setSubmitting(false);
1213
+ };
1214
+ const submitTenant = async (tenantId) => {
1215
+ if (!tenantSel) return;
1216
+ setSubmitting(true);
1217
+ setFormError("");
1218
+ const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-tenant-select`, {
1219
+ method: "POST",
1220
+ headers: { "Content-Type": "application/json" },
1221
+ credentials: "include",
1222
+ body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload() })
1223
+ });
1224
+ const payload = await r.json().catch(() => ({}));
1225
+ if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
1226
+ setSubmitting(false);
1227
+ };
1228
+ const startGoogleLogin = async () => {
1229
+ if (!ctx?.app.defaultClientId) {
1230
+ setFormError("Application is not configured for hosted sign-in.");
1231
+ return;
1232
+ }
1233
+ let pkce;
1234
+ try {
1235
+ const mod = await import("./pkce-7WKV4OIN.mjs");
1236
+ pkce = await mod.createPkcePair();
1237
+ } catch (err) {
1238
+ setFormError(err.message || "Unable to initialize Google sign-in");
1239
+ return;
1240
+ }
1241
+ if (typeof document !== "undefined") {
1242
+ const cookieAttrs = "; path=/; SameSite=Lax" + (window.location.protocol === "https:" ? "; Secure" : "");
1243
+ document.cookie = `iqauth_pkce=${pkce.codeVerifier}${cookieAttrs}`;
1244
+ document.cookie = `iqauth_state=${pkce.state}${cookieAttrs}`;
1245
+ }
1246
+ const params = new URLSearchParams({
1247
+ redirect_uri: returnTo,
1248
+ client_id: ctx.app.defaultClientId,
1249
+ state: pkce.state,
1250
+ nonce: pkce.nonce,
1251
+ code_challenge: pkce.codeChallenge,
1252
+ code_challenge_method: "S256",
1253
+ scope: "openid profile email"
1254
+ });
1255
+ const url = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/google?${params.toString()}`;
1256
+ window.location.href = url;
1257
+ };
1258
+ useEffect(() => {
1259
+ if (loading || error || !ctx) return;
1260
+ if (effectivePrompt === "login") {
1261
+ setSilent("skipped");
1262
+ return;
1263
+ }
1264
+ if (silent !== "idle") return;
1265
+ if (!ctx.session || !ctx.app.defaultClientId || !ctx.returnAllowed) {
1266
+ setSilent("skipped");
1267
+ return;
1268
+ }
1269
+ setSilent("trying");
1270
+ (async () => {
1271
+ try {
1272
+ const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-resume`, {
1273
+ method: "POST",
1274
+ headers: { "Content-Type": "application/json" },
1275
+ credentials: "include",
1276
+ body: JSON.stringify(oidcPayload())
1277
+ });
1278
+ const payload = await r.json().catch(() => ({}));
1279
+ if (payload?.type === "redirect" && payload.redirectUrl) {
1280
+ (onRedirect || ((u) => {
1281
+ window.location.replace(u);
1282
+ }))(payload.redirectUrl);
1283
+ return;
1284
+ }
1285
+ if (payload?.type === "tenant_selection") {
1286
+ setTenantSel({ token: payload.tenantSelectionToken, tenants: payload.tenants || [] });
1287
+ setSilent("failed");
1288
+ return;
1289
+ }
1290
+ setSilent("failed");
1291
+ } catch {
1292
+ setSilent("failed");
1293
+ }
1294
+ })();
1295
+ }, [loading, error, ctx, effectivePrompt]);
1296
+ const switchAccount = (e) => {
1297
+ if (e) e.preventDefault();
1298
+ setForcePrompt(true);
1299
+ setSilent("skipped");
1300
+ setTenantSel(null);
1301
+ if (typeof window !== "undefined") {
1302
+ try {
1303
+ const u = new URL(window.location.href);
1304
+ u.searchParams.set("prompt", "login");
1305
+ window.history.replaceState({}, "", u.toString());
1306
+ } catch {
1307
+ }
1308
+ }
1309
+ };
1310
+ useEffect(() => {
1311
+ if (!ctx?.app.defaultClientId) return;
1312
+ const params = new URLSearchParams(window.location.search);
1313
+ const oauthCode = params.get("code");
1314
+ if (!oauthCode) return;
1315
+ const u = new URL(window.location.href);
1316
+ u.searchParams.delete("code");
1317
+ const codeRedirectUri = u.toString();
1318
+ setOauthExchanging(true);
1319
+ setFormError("");
1320
+ (async () => {
1321
+ try {
1322
+ const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-complete-oauth`, {
1323
+ method: "POST",
1324
+ headers: { "Content-Type": "application/json" },
1325
+ credentials: "include",
1326
+ body: JSON.stringify({
1327
+ authCode: oauthCode,
1328
+ code_redirect_uri: codeRedirectUri,
1329
+ ...oidcPayload()
1330
+ })
1331
+ });
1332
+ const payload = await r.json().catch(() => ({}));
1333
+ try {
1334
+ window.history.replaceState({}, "", u.pathname + (u.search ? u.search : "") + u.hash);
1335
+ } catch {
1336
+ }
1337
+ if (!handlePayload(payload)) setFormError(payload.error_description || payload.error || "Authorization code exchange failed");
1338
+ } catch (err) {
1339
+ setFormError(err.message || "Authorization code exchange failed");
1340
+ }
1341
+ setOauthExchanging(false);
1342
+ })();
1343
+ }, [ctx?.app.defaultClientId]);
1344
+ if (loading || oauthExchanging) return /* @__PURE__ */ jsx(Shell, { appearance, branding: ctx?.branding || null, className, title: oauthExchanging ? t2("signIn.submitting") : t2("common.loading"), children: /* @__PURE__ */ jsx("p", { children: oauthExchanging ? t2("signIn.submitting") : t2("common.loading") }) });
1345
+ if (error || !ctx) return /* @__PURE__ */ jsx(Shell, { appearance, branding: null, className, title: t2("errors.serverError"), children: /* @__PURE__ */ jsx(ErrorBanner, { message: error || t2("errors.serverError") }) });
1346
+ if (!ctx.returnAllowed) return /* @__PURE__ */ jsx(Shell, { appearance, branding: ctx.branding, className, title: t2("errors.generic"), children: /* @__PURE__ */ jsx(ErrorBanner, { message: `returnTo "${returnTo}" is not in this app's allowed origins.` }) });
1347
+ const silentEligible = isSilentSsoEligible(ctx, effectivePrompt);
1348
+ if (silentEligible && silent !== "failed") {
1349
+ return /* @__PURE__ */ jsxs(Shell, { appearance, branding: ctx.branding, className, title: t2("signIn.resumingSession"), subtitle: ctx.session ? `${t2("signIn.subtitle")}, ${ctx.session.name || ctx.session.email}.` : void 0, children: [
1350
+ /* @__PURE__ */ jsx("p", { "data-testid": "text-silent-resume", style: { fontSize: 14, opacity: 0.8 }, children: t2("signIn.resumingSession") }),
1351
+ /* @__PURE__ */ jsx("a", { href: "#", onClick: switchAccount, "data-testid": "link-switch-account", style: { fontSize: 13 }, children: t2("signIn.useDifferentAccount") })
1352
+ ] });
1353
+ }
1354
+ const cardTitle = ctx.branding?.loginHeadline || t2("signIn.titleWithApp", { appName: ctx.app.name });
1355
+ const cardSubtitle = ctx.branding?.loginSubheadline || void 0;
1356
+ return /* @__PURE__ */ jsxs(Shell, { appearance, branding: ctx.branding, className, title: cardTitle, subtitle: cardSubtitle, children: [
1357
+ formError ? /* @__PURE__ */ jsx(ErrorBanner, { message: formError }) : null,
1358
+ tenantSel ? /* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-label": t2("signIn.selectTenant"), style: { display: "flex", flexDirection: "column", gap: 8 }, children: tenantSel.tenants.map((tn) => /* @__PURE__ */ jsxs(
1359
+ "button",
1360
+ {
1361
+ type: "button",
1362
+ "data-iqauth-tenant": tn.tenantId,
1363
+ onClick: () => submitTenant(tn.tenantId),
1364
+ style: { textAlign: "left", padding: "10px 14px", border: "1px solid rgba(15,23,42,0.15)", borderRadius: 8, background: "transparent", color: "inherit", cursor: "pointer" },
1365
+ children: [
1366
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontWeight: 500 }, children: tn.tenantName || tn.tenantSlug || tn.tenantId }),
1367
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 12, opacity: 0.6 }, children: tn.roles.join(", ") })
1368
+ ]
1369
+ },
1370
+ tn.tenantId
1371
+ )) }) : mfa ? /* @__PURE__ */ jsxs("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("mfa.title"), children: [
1372
+ !mfa.backup && mfa.methods.length > 1 ? /* @__PURE__ */ jsx(Field, { label: t2("mfa.title"), children: /* @__PURE__ */ jsx("select", { style: inputStyle(), value: mfa.selected, onChange: (e) => setMfa({ ...mfa, selected: e.target.value }), children: mfa.methods.map((m) => /* @__PURE__ */ jsx("option", { value: m, children: m.toUpperCase() }, m)) }) }) : null,
1373
+ /* @__PURE__ */ jsx(Field, { label: mfa.backup ? t2("mfa.backupCodeLabel") : t2("mfa.totpLabel"), children: /* @__PURE__ */ jsx(
1374
+ "input",
1375
+ {
1376
+ style: { ...inputStyle(), fontFamily: "monospace", textAlign: mfa.backup ? "left" : "center", letterSpacing: mfa.backup ? "0.04em" : "0.3em" },
1377
+ value: mfa.code,
1378
+ onChange: (e) => setMfa({ ...mfa, code: e.target.value }),
1379
+ autoComplete: "one-time-code",
1380
+ inputMode: mfa.backup ? "text" : "numeric"
1381
+ }
1382
+ ) }),
1383
+ /* @__PURE__ */ jsx(PrimaryButton, { type: "submit", disabled: submitting || !mfa.code, children: submitting ? t2("mfa.submitting") : t2("mfa.submit") }),
1384
+ /* @__PURE__ */ jsx(GhostButton, { type: "button", onClick: () => setMfa({ ...mfa, backup: !mfa.backup, code: "" }), children: mfa.backup ? t2("mfa.useAuthenticator") : t2("mfa.useBackupCode") })
1385
+ ] }) : /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
1386
+ ctx.providers?.google ? /* @__PURE__ */ jsxs(Fragment2, { children: [
1387
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "iqauth-sdk-google-btn", onClick: startGoogleLogin, disabled: submitting, "aria-label": ctx.branding?.googleButtonLabel || t2("signIn.continueWithGoogle"), children: [
1388
+ /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", "aria-hidden": "true", children: [
1389
+ /* @__PURE__ */ jsx("path", { fill: "#4285F4", d: "M17.64 9.2c0-.64-.06-1.25-.17-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.71v2.26h2.92a8.78 8.78 0 0 0 2.68-6.61z" }),
1390
+ /* @__PURE__ */ jsx("path", { fill: "#34A853", d: "M9 18c2.43 0 4.47-.81 5.96-2.18l-2.92-2.26a5.4 5.4 0 0 1-8.04-2.83H.96v2.33A9 9 0 0 0 9 18z" }),
1391
+ /* @__PURE__ */ jsx("path", { fill: "#FBBC05", d: "M3.96 10.71A5.41 5.41 0 0 1 3.68 9c0-.59.1-1.17.29-1.71V4.96H.96a9 9 0 0 0 0 8.08l3-2.33z" }),
1392
+ /* @__PURE__ */ jsx("path", { fill: "#EA4335", d: "M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.59A9 9 0 0 0 .96 4.96l3 2.33A5.4 5.4 0 0 1 9 3.58z" })
1393
+ ] }),
1394
+ ctx.branding?.googleButtonLabel || t2("signIn.continueWithGoogle")
1395
+ ] }),
1396
+ /* @__PURE__ */ jsx("div", { role: "separator", "aria-label": t2("common.or"), className: "iqauth-sdk-divider", children: t2("signIn.dividerOr").toUpperCase() })
1397
+ ] }) : null,
1398
+ /* @__PURE__ */ jsxs("form", { onSubmit: submitLogin, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("signIn.titleWithApp", { appName: ctx.app.name }), children: [
1399
+ /* @__PURE__ */ jsx(Field, { label: t2("signIn.emailLabel"), children: /* @__PURE__ */ jsx("input", { style: inputStyle(), type: "email", autoComplete: "email", required: true, value: email, onChange: (e) => setEmail(e.target.value) }) }),
1400
+ /* @__PURE__ */ jsx(Field, { label: t2("signIn.passwordLabel"), children: /* @__PURE__ */ jsx("input", { style: inputStyle(), type: "password", autoComplete: "current-password", required: true, value: password, onChange: (e) => setPassword(e.target.value) }) }),
1401
+ /* @__PURE__ */ jsx(PrimaryButton, { type: "submit", disabled: submitting || !email || !password, children: submitting ? t2("signIn.submitting") : t2("signIn.submit") })
1402
+ ] })
1403
+ ] }),
1404
+ (silent === "failed" || effectivePrompt === "login" && ctx.session) && !mfa ? /* @__PURE__ */ jsx("p", { style: { marginTop: 12, fontSize: 13, opacity: 0.75 }, children: /* @__PURE__ */ jsx("a", { href: "#", onClick: switchAccount, "data-testid": "link-switch-account", children: t2("signIn.useDifferentAccount") }) }) : null
1405
+ ] });
1406
+ }
1407
+ function SignUp({ iqAuthBaseUrl, appKey, returnTo, onSuccess, className }) {
1408
+ const t2 = useT();
1409
+ const localeBundle = useLocale();
1410
+ const { ctx, loading } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo || "");
1411
+ const [name, setName] = useState("");
1412
+ const [email, setEmail] = useState("");
1413
+ const [password, setPassword] = useState("");
1414
+ const [organizationName, setOrganizationName] = useState("");
1415
+ const [submitting, setSubmitting] = useState(false);
1416
+ const [error, setError] = useState("");
1417
+ const [done, setDone] = useState(false);
1418
+ const submit = async (e) => {
1419
+ e.preventDefault();
1420
+ setSubmitting(true);
1421
+ setError("");
1422
+ try {
1423
+ await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/signup`, {
1424
+ method: "POST",
1425
+ headers: { "Content-Type": "application/json" },
1426
+ body: JSON.stringify({ email, name, password, organizationName: organizationName || void 0 })
1427
+ });
1428
+ setDone(true);
1429
+ onSuccess?.();
1430
+ } catch (err) {
1431
+ setError(localizeError(localeBundle, err.message));
1432
+ }
1433
+ setSubmitting(false);
1434
+ };
1435
+ if (loading) return /* @__PURE__ */ jsx(Shell, { branding: null, className, children: /* @__PURE__ */ jsx("p", { children: t2("common.loading") }) });
1436
+ return /* @__PURE__ */ jsx(Shell, { branding: ctx?.branding || null, className, title: t2("signUp.title"), children: done ? /* @__PURE__ */ jsx("div", { role: "status", children: /* @__PURE__ */ jsx("p", { children: t2("magicLink.subtitle", { email }) }) }) : /* @__PURE__ */ jsxs("form", { onSubmit: submit, style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
1437
+ error ? /* @__PURE__ */ jsx(ErrorBanner, { message: error }) : null,
1438
+ /* @__PURE__ */ jsx(Field, { label: t2("signUp.nameLabel"), children: /* @__PURE__ */ jsx("input", { style: inputStyle(), value: name, onChange: (e) => setName(e.target.value), required: true }) }),
1439
+ /* @__PURE__ */ jsx(Field, { label: t2("signUp.emailLabel"), children: /* @__PURE__ */ jsx("input", { style: inputStyle(), type: "email", autoComplete: "email", value: email, onChange: (e) => setEmail(e.target.value), required: true }) }),
1440
+ /* @__PURE__ */ jsx(Field, { label: `${t2("signUp.tenantNameLabel")} (${t2("common.optional")})`, children: /* @__PURE__ */ jsx("input", { style: inputStyle(), value: organizationName, onChange: (e) => setOrganizationName(e.target.value) }) }),
1441
+ /* @__PURE__ */ jsx(Field, { label: t2("signUp.passwordLabel"), children: /* @__PURE__ */ jsx("input", { style: inputStyle(), type: "password", autoComplete: "new-password", minLength: 8, value: password, onChange: (e) => setPassword(e.target.value), required: true }) }),
1442
+ /* @__PURE__ */ jsx(PrimaryButton, { type: "submit", disabled: submitting || !email || !password || !name, children: submitting ? t2("signUp.submitting") : t2("signUp.submit") })
1443
+ ] }) });
1444
+ }
1445
+ function initialsOf(name, email) {
1446
+ const src = name || email || "?";
1447
+ const parts = src.split(/[\s@]+/).filter(Boolean);
1448
+ if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
1449
+ return src.substring(0, 2).toUpperCase();
1450
+ }
1451
+ function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
1452
+ const t2 = useT();
1453
+ const [user, setUser] = useState(null);
1454
+ const [open, setOpen] = useState(false);
1455
+ const branding = useResolvedSdkBranding(iqAuthBaseUrl);
1456
+ const accent = branding?.accentColor || "#6366f1";
1457
+ const primary = branding?.primaryColor || "#0f172a";
1458
+ useEffect(() => {
1459
+ let cancelled = false;
1460
+ fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
1461
+ if (!cancelled && p?.data) setUser(p.data);
1462
+ }).catch(() => {
1463
+ });
1464
+ return () => {
1465
+ cancelled = true;
1466
+ };
1467
+ }, [iqAuthBaseUrl]);
1468
+ const signOut2 = async () => {
1469
+ try {
1470
+ await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/logout`, { method: "POST", credentials: "include" });
1471
+ } catch {
1472
+ }
1473
+ if (onSignOut) onSignOut();
1474
+ else window.location.reload();
1475
+ };
1476
+ if (!user) return null;
1477
+ const target = accountUrl || `${iqAuthBaseUrl.replace(/\/$/, "")}/account`;
1478
+ return /* @__PURE__ */ jsxs(
1479
+ "div",
1480
+ {
1481
+ className,
1482
+ style: { position: "relative", display: "inline-block", ...brandStyle(branding) },
1483
+ "data-iqauth-sdk-userbutton": "",
1484
+ "data-branding-rev": branding?.brandingRev || "",
1485
+ children: [
1486
+ /* @__PURE__ */ jsx(
1487
+ "button",
1488
+ {
1489
+ type: "button",
1490
+ "aria-haspopup": "menu",
1491
+ "aria-expanded": open,
1492
+ onClick: () => setOpen((o) => !o),
1493
+ style: {
1494
+ width: 32,
1495
+ height: 32,
1496
+ borderRadius: "50%",
1497
+ background: accent,
1498
+ color: "#fff",
1499
+ border: "none",
1500
+ cursor: "pointer",
1501
+ fontSize: 12,
1502
+ fontWeight: 600
1503
+ },
1504
+ children: user.picture ? /* @__PURE__ */ jsx("img", { src: user.picture, alt: user.name, style: { width: "100%", height: "100%", borderRadius: "50%" } }) : initialsOf(user.name, user.email)
1505
+ }
1506
+ ),
1507
+ open ? /* @__PURE__ */ jsxs("div", { role: "menu", style: {
1508
+ position: "absolute",
1509
+ right: 0,
1510
+ top: 40,
1511
+ minWidth: 200,
1512
+ background: "#fff",
1513
+ border: "1px solid rgba(15,23,42,0.12)",
1514
+ borderRadius: 8,
1515
+ boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
1516
+ padding: 8,
1517
+ zIndex: 100
1518
+ }, children: [
1519
+ /* @__PURE__ */ jsxs("div", { style: { padding: "8px 10px", fontSize: 12, opacity: 0.7, borderBottom: "1px solid rgba(15,23,42,0.06)" }, children: [
1520
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500, color: "#0f172a" }, children: user.name }),
1521
+ /* @__PURE__ */ jsx("div", { children: user.email })
1522
+ ] }),
1523
+ /* @__PURE__ */ jsx("a", { href: target, role: "menuitem", style: { display: "block", padding: "8px 10px", fontSize: 13, color: primary, textDecoration: "none" }, children: t2("userButton.manageAccount") }),
1524
+ /* @__PURE__ */ jsx(
1525
+ "button",
1526
+ {
1527
+ role: "menuitem",
1528
+ type: "button",
1529
+ onClick: signOut2,
1530
+ style: { display: "block", width: "100%", textAlign: "left", padding: "8px 10px", fontSize: 13, background: "transparent", border: "none", cursor: "pointer", color: "#b91c1c" },
1531
+ children: t2("userButton.signOut")
1532
+ }
1533
+ )
1534
+ ] }) : null
1535
+ ]
1536
+ }
1537
+ );
1538
+ }
1539
+ function UserProfile({ iqAuthBaseUrl, className }) {
1540
+ const t2 = useT();
1541
+ const localeBundle = useLocale();
1542
+ const branding = useResolvedSdkBranding(iqAuthBaseUrl);
1543
+ const [user, setUser] = useState(null);
1544
+ const [oldPassword, setOldPassword] = useState("");
1545
+ const [newPassword, setNewPassword] = useState("");
1546
+ const [pwState, setPwState] = useState({ submitting: false, message: "", error: "" });
1547
+ const [sessions, setSessions] = useState([]);
1548
+ const [revokeAllBusy, setRevokeAllBusy] = useState(false);
1549
+ const loadSessions = () => {
1550
+ fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/users/me/sessions`, { credentials: "include" }).then((r) => r.ok ? r.json() : Promise.reject(r)).then((p) => setSessions(p?.data?.sessions || [])).catch(() => {
1551
+ fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/sessions`, { credentials: "include" }).then((r) => r.json()).then((p) => setSessions(p?.data?.sessions || p?.data || [])).catch(() => {
1552
+ });
1553
+ });
1554
+ };
1555
+ useEffect(() => {
1556
+ fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
1557
+ if (p?.data) setUser(p.data);
1558
+ }).catch(() => {
1559
+ });
1560
+ loadSessions();
1561
+ }, [iqAuthBaseUrl]);
1562
+ const changePassword = async (e) => {
1563
+ e.preventDefault();
1564
+ setPwState({ submitting: true, message: "", error: "" });
1565
+ try {
1566
+ await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/password/change`, {
1567
+ method: "POST",
1568
+ headers: { "Content-Type": "application/json" },
1569
+ body: JSON.stringify({ oldPassword, newPassword })
1570
+ });
1571
+ setPwState({ submitting: false, message: t(localeBundle, "userProfile.passwordUpdated"), error: "" });
1572
+ setOldPassword("");
1573
+ setNewPassword("");
1574
+ } catch (err) {
1575
+ setPwState({ submitting: false, message: "", error: localizeError(localeBundle, err.message) });
1576
+ }
1577
+ };
1578
+ const revoke = async (sessionId) => {
1579
+ const newRes = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/users/me/sessions/${sessionId}`, { method: "DELETE", credentials: "include" });
1580
+ if (!newRes.ok && newRes.status === 404) {
1581
+ await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/sessions/${sessionId}`, { method: "DELETE", credentials: "include" });
1582
+ }
1583
+ setSessions((prev) => prev.filter((s) => s.id !== sessionId));
1584
+ };
1585
+ const revokeAllOthers = async () => {
1586
+ setRevokeAllBusy(true);
1587
+ try {
1588
+ await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/users/me/sessions/revoke-all-others`, { method: "POST", credentials: "include" });
1589
+ setSessions((prev) => prev.filter((s) => s.isCurrent));
1590
+ } finally {
1591
+ setRevokeAllBusy(false);
1592
+ }
1593
+ };
1594
+ if (!user) return /* @__PURE__ */ jsx(Shell, { branding, className, children: /* @__PURE__ */ jsx("p", { children: t2("common.loading") }) });
1595
+ return /* @__PURE__ */ jsxs(Shell, { branding, className, title: t2("userProfile.title"), children: [
1596
+ /* @__PURE__ */ jsxs("section", { "aria-labelledby": "iqauth-profile", style: { marginBottom: 20 }, children: [
1597
+ /* @__PURE__ */ jsx("h3", { id: "iqauth-profile", style: { fontSize: 14, fontWeight: 600 }, children: t2("userProfile.profileTab") }),
1598
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, margin: "4px 0" }, children: [
1599
+ /* @__PURE__ */ jsxs("strong", { children: [
1600
+ t2("common.name"),
1601
+ ":"
1602
+ ] }),
1603
+ " ",
1604
+ user.name
1605
+ ] }),
1606
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, margin: "4px 0" }, children: [
1607
+ /* @__PURE__ */ jsxs("strong", { children: [
1608
+ t2("common.email"),
1609
+ ":"
1610
+ ] }),
1611
+ " ",
1612
+ user.email
1613
+ ] })
1614
+ ] }),
1615
+ /* @__PURE__ */ jsxs("section", { "aria-labelledby": "iqauth-pw", style: { marginBottom: 20 }, children: [
1616
+ /* @__PURE__ */ jsx("h3", { id: "iqauth-pw", style: { fontSize: 14, fontWeight: 600 }, children: t2("userProfile.changePassword") }),
1617
+ pwState.error ? /* @__PURE__ */ jsx(ErrorBanner, { message: pwState.error }) : null,
1618
+ pwState.message ? /* @__PURE__ */ jsx("div", { role: "status", style: { fontSize: 13, color: "#047857" }, children: pwState.message }) : null,
1619
+ /* @__PURE__ */ jsxs("form", { onSubmit: changePassword, style: { display: "flex", flexDirection: "column", gap: 10 }, children: [
1620
+ /* @__PURE__ */ jsx(Field, { label: t2("userProfile.currentPassword"), children: /* @__PURE__ */ jsx("input", { style: inputStyle(), type: "password", autoComplete: "current-password", value: oldPassword, onChange: (e) => setOldPassword(e.target.value), required: true }) }),
1621
+ /* @__PURE__ */ jsx(Field, { label: t2("userProfile.newPassword"), children: /* @__PURE__ */ jsx("input", { style: inputStyle(), type: "password", autoComplete: "new-password", minLength: 8, value: newPassword, onChange: (e) => setNewPassword(e.target.value), required: true }) }),
1622
+ /* @__PURE__ */ jsx(PrimaryButton, { type: "submit", disabled: pwState.submitting || !oldPassword || !newPassword, children: pwState.submitting ? t2("common.saving") : t2("userProfile.changePassword") })
1623
+ ] })
1624
+ ] }),
1625
+ /* @__PURE__ */ jsxs("section", { "aria-labelledby": "iqauth-sessions", children: [
1626
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1627
+ /* @__PURE__ */ jsx("h3", { id: "iqauth-sessions", style: { fontSize: 14, fontWeight: 600, margin: 0 }, children: t2("userProfile.sessionsTab") }),
1628
+ sessions.some((s) => !s.isCurrent) && /* @__PURE__ */ jsx(
1629
+ "button",
1630
+ {
1631
+ type: "button",
1632
+ disabled: revokeAllBusy,
1633
+ onClick: revokeAllOthers,
1634
+ style: { fontSize: 12, color: "#b91c1c", background: "transparent", border: "1px solid #fecaca", borderRadius: 4, padding: "4px 10px", cursor: "pointer" },
1635
+ children: revokeAllBusy ? t2("common.submitting") : t2("userProfile.revokeAllOthers")
1636
+ }
1637
+ )
1638
+ ] }),
1639
+ sessions.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, opacity: 0.6 }, children: t2("userProfile.sessionsEmpty") }) : /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: "8px 0 0", display: "flex", flexDirection: "column", gap: 6 }, children: sessions.map((s) => /* @__PURE__ */ jsxs("li", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 13, padding: "8px 10px", background: s.isCurrent ? "#ecfdf5" : "#f8fafc", borderRadius: 6, border: s.isCurrent ? "1px solid #a7f3d0" : "1px solid transparent" }, children: [
1640
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 2 }, children: [
1641
+ /* @__PURE__ */ jsxs("span", { style: { fontWeight: 500 }, children: [
1642
+ s.device || s.userAgent || s.deviceName || "\u2014",
1643
+ s.isCurrent && /* @__PURE__ */ jsxs("span", { style: { marginLeft: 8, fontSize: 11, color: "#047857" }, children: [
1644
+ "(",
1645
+ t2("userProfile.thisDevice"),
1646
+ ")"
1647
+ ] })
1648
+ ] }),
1649
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 11, opacity: 0.65 }, children: [
1650
+ s.ip || "\u2014",
1651
+ s.lastActiveAt ? ` \xB7 ${new Date(s.lastActiveAt).toLocaleString()}` : ""
1652
+ ] })
1653
+ ] }),
1654
+ !s.isCurrent && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => revoke(s.id), style: { fontSize: 12, color: "#b91c1c", background: "transparent", border: "1px solid #fecaca", borderRadius: 4, padding: "2px 8px", cursor: "pointer" }, children: t2("userProfile.revokeSession") })
1655
+ ] }, s.id)) })
1656
+ ] })
1657
+ ] });
1658
+ }
1659
+ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearance, className }) {
1660
+ const t2 = useT();
1661
+ const branding = useResolvedSdkBranding(iqAuthBaseUrl);
1662
+ const accent = branding?.accentColor || "#6366f1";
1663
+ const [memberships, setMemberships] = useState([]);
1664
+ const [activeTenantId, setActiveTenantId] = useState(null);
1665
+ const [open, setOpen] = useState(false);
1666
+ useEffect(() => {
1667
+ fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
1668
+ if (p?.data?.tenantId) setActiveTenantId(p.data.tenantId);
1669
+ }).catch(() => {
1670
+ });
1671
+ fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants/memberships`, { credentials: "include" }).then((r) => r.json()).then((p) => setMemberships(p?.data?.memberships || p?.data || [])).catch(() => {
1672
+ });
1673
+ }, [iqAuthBaseUrl]);
1674
+ const switchTo = async (tenantId) => {
1675
+ try {
1676
+ await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/select-tenant`, {
1677
+ method: "POST",
1678
+ headers: { "Content-Type": "application/json" },
1679
+ body: JSON.stringify({ tenantId })
1680
+ });
1681
+ setActiveTenantId(tenantId);
1682
+ setOpen(false);
1683
+ onSwitched?.(tenantId);
1684
+ } catch {
1685
+ }
1686
+ };
1687
+ const active = memberships.find((m) => m.tenantId === activeTenantId);
1688
+ return /* @__PURE__ */ jsxs(
1689
+ "div",
1690
+ {
1691
+ className,
1692
+ style: { position: "relative", display: "inline-block", ...brandStyle(branding) },
1693
+ "data-iqauth-sdk-orgswitcher": "",
1694
+ "data-branding-rev": branding?.brandingRev || "",
1695
+ children: [
1696
+ /* @__PURE__ */ jsx(
1697
+ "button",
1698
+ {
1699
+ type: "button",
1700
+ "aria-haspopup": "menu",
1701
+ "aria-expanded": open,
1702
+ onClick: () => setOpen((o) => !o),
1703
+ style: { background: "transparent", border: `1px solid ${accent}55`, color: branding?.primaryColor || "#0f172a", padding: "6px 12px", borderRadius: 6, cursor: "pointer", fontSize: 13 },
1704
+ children: active?.tenantName || active?.tenantSlug || t2("orgSwitcher.label")
1705
+ }
1706
+ ),
1707
+ open ? /* @__PURE__ */ jsx("div", { role: "menu", style: {
1708
+ position: "absolute",
1709
+ left: 0,
1710
+ top: 36,
1711
+ minWidth: 220,
1712
+ background: "#fff",
1713
+ border: "1px solid rgba(15,23,42,0.12)",
1714
+ borderRadius: 8,
1715
+ boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
1716
+ padding: 8,
1717
+ zIndex: 100
1718
+ }, children: memberships.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, opacity: 0.6, padding: "4px 6px" }, children: t2("orgSwitcher.noOrgs") }) : memberships.map((m) => /* @__PURE__ */ jsxs(
1719
+ "button",
1720
+ {
1721
+ role: "menuitem",
1722
+ type: "button",
1723
+ onClick: () => switchTo(m.tenantId),
1724
+ style: {
1725
+ display: "block",
1726
+ width: "100%",
1727
+ textAlign: "left",
1728
+ padding: "8px 10px",
1729
+ background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
1730
+ border: "none",
1731
+ borderRadius: 4,
1732
+ cursor: "pointer",
1733
+ fontSize: 13,
1734
+ color: "#0f172a"
1735
+ },
1736
+ children: [
1737
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
1738
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
1739
+ ]
1740
+ },
1741
+ m.tenantId
1742
+ )) }) : null
1743
+ ]
1744
+ }
1745
+ );
1746
+ }
1747
+ function useImpersonation() {
1748
+ const { snapshot } = useCtx();
1749
+ return useMemo(() => {
1750
+ const claims = snapshot.claims;
1751
+ const isImpersonating = claims?.purpose === "impersonation" && !!claims?.act?.sub;
1752
+ return {
1753
+ isImpersonating,
1754
+ actor: isImpersonating ? claims.act : null,
1755
+ target: isImpersonating ? snapshot.user : null
1756
+ };
1757
+ }, [snapshot]);
1758
+ }
1759
+ function ImpersonationBanner({ render, onExit, className, style } = {}) {
1760
+ const t2 = useT();
1761
+ const info = useImpersonation();
1762
+ const { manager } = useCtx();
1763
+ const exit = useCallback(async () => {
1764
+ if (onExit) return void onExit();
1765
+ const { exitImpersonation } = await import("./reverify-C64QXKJO.mjs");
1766
+ const restored = exitImpersonation(manager);
1767
+ if (restored) return;
1768
+ const { signOut: signOut2 } = await import("./signIn-SHBW6Z4T.mjs");
1769
+ await signOut2(manager);
1770
+ }, [manager, onExit]);
1771
+ if (!info.isImpersonating) return null;
1772
+ if (render) return createElement(Fragment, null, render({ ...info, exit }));
1773
+ const targetLabel = info.target?.email || info.target?.name || info.target?.sub || "user";
1774
+ const _actorLabel = info.actor?.email || info.actor?.name || info.actor?.sub || "admin";
1775
+ void _actorLabel;
1776
+ return createElement(
1777
+ "div",
1778
+ {
1779
+ role: "alert",
1780
+ className,
1781
+ style: {
1782
+ position: "sticky",
1783
+ top: 0,
1784
+ left: 0,
1785
+ right: 0,
1786
+ zIndex: 9999,
1787
+ background: "#b91c1c",
1788
+ color: "#fff",
1789
+ padding: "8px 16px",
1790
+ display: "flex",
1791
+ alignItems: "center",
1792
+ justifyContent: "space-between",
1793
+ fontSize: 13,
1794
+ fontFamily: "system-ui, sans-serif",
1795
+ ...style
1796
+ }
1797
+ },
1798
+ createElement(
1799
+ "span",
1800
+ null,
1801
+ t2("impersonation.banner", { targetEmail: targetLabel })
1802
+ ),
1803
+ createElement(
1804
+ "button",
1805
+ {
1806
+ type: "button",
1807
+ onClick: exit,
1808
+ style: {
1809
+ background: "rgba(255,255,255,0.18)",
1810
+ color: "#fff",
1811
+ border: "1px solid rgba(255,255,255,0.4)",
1812
+ borderRadius: 4,
1813
+ padding: "4px 10px",
1814
+ cursor: "pointer",
1815
+ fontSize: 12
1816
+ }
1817
+ },
1818
+ t2("impersonation.exit")
1819
+ )
1820
+ );
1821
+ }
1822
+ function useReverification(fn, opts = {}) {
1823
+ const { manager } = useCtx();
1824
+ const level = opts.level ?? "password";
1825
+ return (async (...args) => {
1826
+ let token = null;
1827
+ let res = await fn(token)(...args);
1828
+ const code = await peekErrorCode(res);
1829
+ const isReverifyError = res.status === 401 && code && (code === "REVERIFICATION_REQUIRED" || code === "REVERIFICATION_EXPIRED" || code === "REVERIFICATION_INVALID" || code === "REVERIFICATION_USED" || code === "REVERIFICATION_LEVEL_INSUFFICIENT");
1830
+ if (!isReverifyError) return res;
1831
+ const prompt = opts.prompt ?? defaultReverifyPrompt;
1832
+ const credentials = await prompt(level);
1833
+ if (!credentials) {
1834
+ throw new Error("Reverification cancelled");
1835
+ }
1836
+ const { reverify } = await import("./reverify-C64QXKJO.mjs");
1837
+ const minted = await reverify(manager, { level, ...credentials });
1838
+ token = minted.token;
1839
+ res = await fn(token)(...args);
1840
+ return res;
1841
+ });
1842
+ }
1843
+ async function peekErrorCode(res) {
1844
+ try {
1845
+ const cloned = res.clone();
1846
+ const body = await cloned.json();
1847
+ return body?.error?.code ?? null;
1848
+ } catch {
1849
+ return null;
1850
+ }
1851
+ }
1852
+ async function defaultReverifyPrompt(level) {
1853
+ if (typeof window === "undefined") return null;
1854
+ if (level === "password") {
1855
+ const password = window.prompt("Confirm your password to continue");
1856
+ if (!password) return null;
1857
+ return { password };
1858
+ }
1859
+ const totp = window.prompt("Enter your MFA code to continue");
1860
+ if (!totp) return null;
1861
+ return { totp, method: "totp" };
1862
+ }
1863
+ function slugify(input) {
1864
+ return input.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
1865
+ }
1866
+ function CreateOrganization({ iqAuthBaseUrl, onCreated, redirectUrl, unstyled, appearance, className }) {
1867
+ const branding = useResolvedSdkBranding(iqAuthBaseUrl);
1868
+ const [name, setName] = useState("");
1869
+ const [slug, setSlug] = useState("");
1870
+ const [slugTouched, setSlugTouched] = useState(false);
1871
+ const [submitting, setSubmitting] = useState(false);
1872
+ const [error, setError] = useState(null);
1873
+ const [created, setCreated] = useState(null);
1874
+ const [slugCheck, setSlugCheck] = useState({ status: "idle" });
1875
+ useEffect(() => {
1876
+ if (!slugTouched) setSlug(slugify(name));
1877
+ }, [name, slugTouched]);
1878
+ useEffect(() => {
1879
+ const s = slug.trim().toLowerCase();
1880
+ if (!s) {
1881
+ setSlugCheck({ status: "idle" });
1882
+ return;
1883
+ }
1884
+ if (!/^[a-z0-9-]{2,64}$/.test(s)) {
1885
+ setSlugCheck({ status: "invalid", checked: s });
1886
+ return;
1887
+ }
1888
+ setSlugCheck({ status: "checking", checked: s });
1889
+ const handle = setTimeout(async () => {
1890
+ try {
1891
+ const res = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants/check-slug?slug=${encodeURIComponent(s)}`, {
1892
+ credentials: "include",
1893
+ headers: { Accept: "application/json" }
1894
+ });
1895
+ const body = await res.json().catch(() => ({}));
1896
+ const data = body.data;
1897
+ if (!data) {
1898
+ setSlugCheck({ status: "idle", checked: s });
1899
+ return;
1900
+ }
1901
+ if (data.reason === "INVALID_FORMAT") {
1902
+ setSlugCheck({ status: "invalid", checked: s });
1903
+ return;
1904
+ }
1905
+ setSlugCheck({ status: data.available ? "available" : "taken", checked: s });
1906
+ } catch {
1907
+ setSlugCheck({ status: "idle", checked: s });
1908
+ }
1909
+ }, 350);
1910
+ return () => clearTimeout(handle);
1911
+ }, [slug, iqAuthBaseUrl]);
1912
+ const submit = async (e) => {
1913
+ e.preventDefault();
1914
+ setSubmitting(true);
1915
+ setError(null);
1916
+ try {
1917
+ const res = await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants`, {
1918
+ method: "POST",
1919
+ headers: { "Content-Type": "application/json" },
1920
+ credentials: "include",
1921
+ body: JSON.stringify({ name: name.trim(), slug: slug.trim() })
1922
+ });
1923
+ const payload = res;
1924
+ const tenant = payload.data ?? payload;
1925
+ const next = { id: tenant.id, name: tenant.name, slug: tenant.slug };
1926
+ setCreated(next);
1927
+ onCreated?.(next);
1928
+ setName("");
1929
+ setSlug("");
1930
+ setSlugTouched(false);
1931
+ if (redirectUrl && typeof window !== "undefined") {
1932
+ const url = typeof redirectUrl === "function" ? redirectUrl(next) : redirectUrl;
1933
+ if (url) window.location.assign(url);
1934
+ }
1935
+ } catch (err) {
1936
+ setError(err instanceof Error ? err.message : "Failed to create organization");
1937
+ } finally {
1938
+ setSubmitting(false);
1939
+ }
1940
+ };
1941
+ const form = /* @__PURE__ */ jsxs("form", { onSubmit: submit, style: { display: "flex", flexDirection: "column", gap: 12 }, "data-iqauth-sdk-create-org": "", "aria-labelledby": "iqauth-create-org-heading", children: [
1942
+ error ? /* @__PURE__ */ jsx(ErrorBanner, { message: error }) : null,
1943
+ created ? /* @__PURE__ */ jsxs("p", { role: "status", style: { fontSize: 13, color: "#047857" }, children: [
1944
+ "Organization \u201C",
1945
+ created.name,
1946
+ "\u201D created."
1947
+ ] }) : null,
1948
+ /* @__PURE__ */ jsx(Field, { label: "Organization name", children: /* @__PURE__ */ jsx("input", { "data-testid": "input-create-org-name", autoFocus: true, style: inputStyle(), value: name, onChange: (e) => setName(e.target.value), required: true, minLength: 2, "aria-required": "true" }) }),
1949
+ /* @__PURE__ */ jsxs(Field, { label: "Organization slug", children: [
1950
+ /* @__PURE__ */ jsx(
1951
+ "input",
1952
+ {
1953
+ "data-testid": "input-create-org-slug",
1954
+ style: { ...inputStyle(), fontFamily: "monospace" },
1955
+ value: slug,
1956
+ onChange: (e) => {
1957
+ setSlugTouched(true);
1958
+ setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-"));
1959
+ },
1960
+ required: true,
1961
+ pattern: "[a-z0-9-]+",
1962
+ minLength: 2,
1963
+ "aria-required": "true",
1964
+ "aria-describedby": "iqauth-create-org-slug-hint",
1965
+ "aria-invalid": slugCheck.status === "taken" || slugCheck.status === "invalid"
1966
+ }
1967
+ ),
1968
+ /* @__PURE__ */ jsx(
1969
+ "p",
1970
+ {
1971
+ id: "iqauth-create-org-slug-hint",
1972
+ "data-testid": "text-create-org-slug-status",
1973
+ role: "status",
1974
+ "aria-live": "polite",
1975
+ style: { fontSize: 12, marginTop: 4, color: slugCheck.status === "taken" || slugCheck.status === "invalid" ? "#b91c1c" : slugCheck.status === "available" ? "#047857" : "inherit", opacity: slugCheck.status === "checking" ? 0.6 : 1 },
1976
+ children: slugCheck.status === "checking" ? "Checking availability\u2026" : slugCheck.status === "available" ? "Slug is available." : slugCheck.status === "taken" ? "That slug is taken." : slugCheck.status === "invalid" ? "Slugs must be 2\u201364 chars, lowercase letters/numbers/hyphens." : "We'll auto-generate a slug from the name; you can override it."
1977
+ }
1978
+ )
1979
+ ] }),
1980
+ /* @__PURE__ */ jsx(
1981
+ PrimaryButton,
1982
+ {
1983
+ "data-testid": "button-create-org-submit",
1984
+ type: "submit",
1985
+ disabled: submitting || !name.trim() || !slug.trim() || slugCheck.status === "taken" || slugCheck.status === "invalid" || slugCheck.status === "checking",
1986
+ children: submitting ? "Creating\u2026" : "Create organization"
1987
+ }
1988
+ )
1989
+ ] });
1990
+ if (unstyled) {
1991
+ return /* @__PURE__ */ jsxs("div", { className, "data-iqauth-sdk-create-org-bare": "", children: [
1992
+ /* @__PURE__ */ jsx("h3", { id: "iqauth-create-org-heading", style: { fontSize: 14, fontWeight: 600, margin: "0 0 12px" }, children: "Create organization" }),
1993
+ form
1994
+ ] });
1995
+ }
1996
+ return /* @__PURE__ */ jsx(Shell, { appearance, branding, className, title: "Create organization", subtitle: "Spin up a new tenant for this app.", children: form });
1997
+ }
1998
+ function OrganizationProfile({ iqAuthBaseUrl, tenantId: tenantIdProp, tabs, onDeleted, appearance, className }) {
1999
+ const branding = useResolvedSdkBranding(iqAuthBaseUrl);
2000
+ const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
2001
+ const visibleTabs = tabs && tabs.length > 0 ? tabs : ["general", "members", "invitations", "danger"];
2002
+ const [activeTab, setActiveTab] = useState(visibleTabs[0]);
2003
+ const [tenantId, setTenantId] = useState(tenantIdProp || null);
2004
+ const [tenant, setTenant] = useState(null);
2005
+ const [members, setMembers] = useState([]);
2006
+ const [pendingInvites, setPendingInvites] = useState([]);
2007
+ const [loading, setLoading] = useState(true);
2008
+ const [invitesLoading, setInvitesLoading] = useState(false);
2009
+ const [error, setError] = useState(null);
2010
+ const [renameValue, setRenameValue] = useState("");
2011
+ const [slugValue, setSlugValue] = useState("");
2012
+ const [renameSubmitting, setRenameSubmitting] = useState(false);
2013
+ const [inviteEmail, setInviteEmail] = useState("");
2014
+ const [inviteRole, setInviteRole] = useState("tenant_member");
2015
+ const [inviteSubmitting, setInviteSubmitting] = useState(false);
2016
+ const [actionMessage, setActionMessage] = useState(null);
2017
+ const [confirmDeleteText, setConfirmDeleteText] = useState("");
2018
+ const [confirmDeletePassword, setConfirmDeletePassword] = useState("");
2019
+ const [deleteSubmitting, setDeleteSubmitting] = useState(false);
2020
+ const { user } = useUser();
2021
+ const callerRole = user?.role || null;
2022
+ const callerIsAdmin = callerRole === "tenant_admin" || callerRole === "platform_admin";
2023
+ const visibleTabsFiltered = visibleTabs.filter((t2) => t2 !== "danger" || callerIsAdmin);
2024
+ useEffect(() => {
2025
+ let cancelled = false;
2026
+ (async () => {
2027
+ try {
2028
+ let tid = tenantIdProp || null;
2029
+ if (!tid) {
2030
+ const me = await fetch(`${baseUrl}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json());
2031
+ tid = me?.data?.tenantId || null;
2032
+ }
2033
+ if (!tid) {
2034
+ if (!cancelled) {
2035
+ setError("No active tenant");
2036
+ setLoading(false);
2037
+ }
2038
+ return;
2039
+ }
2040
+ const t2 = await fetch(`${baseUrl}/api/v1/tenants/${tid}`, { credentials: "include" }).then((r) => r.json());
2041
+ const m = await fetch(`${baseUrl}/api/v1/tenants/${tid}/users`, { credentials: "include" }).then((r) => r.json());
2042
+ if (cancelled) return;
2043
+ setTenantId(tid);
2044
+ setTenant(t2?.data ? { id: t2.data.id, name: t2.data.name, slug: t2.data.slug } : null);
2045
+ setRenameValue(t2?.data?.name || "");
2046
+ setSlugValue(t2?.data?.slug || "");
2047
+ const rows = m?.data || [];
2048
+ setMembers(rows.map((row) => ({
2049
+ userId: String(row.userId ?? row.user?.id ?? row.id ?? ""),
2050
+ email: String(row.email ?? row.user?.email ?? ""),
2051
+ name: String(row.name ?? row.user?.name ?? ""),
2052
+ role: String(row.role ?? row.roles?.[0] ?? "tenant_member"),
2053
+ joinedAt: row.joinedAt ?? row.createdAt
2054
+ })));
2055
+ } catch (err) {
2056
+ if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load organization");
2057
+ } finally {
2058
+ if (!cancelled) setLoading(false);
2059
+ }
2060
+ })();
2061
+ return () => {
2062
+ cancelled = true;
2063
+ };
2064
+ }, [baseUrl, tenantIdProp]);
2065
+ const loadInvites = async (tid) => {
2066
+ setInvitesLoading(true);
2067
+ try {
2068
+ const r = await fetch(`${baseUrl}/api/v1/invites?tenantId=${encodeURIComponent(tid)}&status=pending`, { credentials: "include" }).then((res) => res.json());
2069
+ const rows = r?.data || [];
2070
+ setPendingInvites(rows.map((inv) => ({
2071
+ id: String(inv.id),
2072
+ email: String(inv.email),
2073
+ role: String(inv.role),
2074
+ status: String(inv.status),
2075
+ expiresAt: inv.expiresAt,
2076
+ createdAt: inv.createdAt
2077
+ })));
2078
+ } catch (err) {
2079
+ setError(err instanceof Error ? err.message : "Failed to load invitations");
2080
+ } finally {
2081
+ setInvitesLoading(false);
2082
+ }
2083
+ };
2084
+ useEffect(() => {
2085
+ if (activeTab === "invitations" && tenantId) loadInvites(tenantId);
2086
+ }, [activeTab, tenantId]);
2087
+ const reloadMembers = async () => {
2088
+ if (!tenantId) return;
2089
+ const m = await fetch(`${baseUrl}/api/v1/tenants/${tenantId}/users`, { credentials: "include" }).then((r) => r.json());
2090
+ const rows = m?.data || [];
2091
+ setMembers(rows.map((row) => ({
2092
+ userId: String(row.userId ?? row.user?.id ?? row.id ?? ""),
2093
+ email: String(row.email ?? row.user?.email ?? ""),
2094
+ name: String(row.name ?? row.user?.name ?? ""),
2095
+ role: String(row.role ?? row.roles?.[0] ?? "tenant_member"),
2096
+ joinedAt: row.joinedAt ?? row.createdAt
2097
+ })));
2098
+ };
2099
+ const submitRename = async (e) => {
2100
+ e.preventDefault();
2101
+ if (!tenantId) return;
2102
+ setRenameSubmitting(true);
2103
+ setError(null);
2104
+ try {
2105
+ const body = {};
2106
+ if (renameValue.trim() && renameValue.trim() !== tenant?.name) body.name = renameValue.trim();
2107
+ if (slugValue.trim() && slugValue.trim() !== tenant?.slug) body.slug = slugValue.trim();
2108
+ if (Object.keys(body).length === 0) {
2109
+ setRenameSubmitting(false);
2110
+ return;
2111
+ }
2112
+ const res = await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}`, {
2113
+ method: "PATCH",
2114
+ headers: { "Content-Type": "application/json" },
2115
+ credentials: "include",
2116
+ body: JSON.stringify(body)
2117
+ });
2118
+ const t2 = res?.data ?? res;
2119
+ setTenant({ id: t2.id, name: t2.name, slug: t2.slug });
2120
+ setActionMessage("Organization saved.");
2121
+ } catch (err) {
2122
+ setError(err instanceof Error ? err.message : "Failed to save");
2123
+ } finally {
2124
+ setRenameSubmitting(false);
2125
+ }
2126
+ };
2127
+ const submitInvite = async (e) => {
2128
+ e.preventDefault();
2129
+ if (!tenantId) return;
2130
+ setInviteSubmitting(true);
2131
+ setError(null);
2132
+ try {
2133
+ await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}/users/invite`, {
2134
+ method: "POST",
2135
+ headers: { "Content-Type": "application/json" },
2136
+ credentials: "include",
2137
+ body: JSON.stringify({ email: inviteEmail.trim(), role: inviteRole })
2138
+ });
2139
+ setActionMessage(`Invitation sent to ${inviteEmail.trim()}.`);
2140
+ setInviteEmail("");
2141
+ if (activeTab === "invitations") await loadInvites(tenantId);
2142
+ } catch (err) {
2143
+ setError(err instanceof Error ? err.message : "Failed to invite");
2144
+ } finally {
2145
+ setInviteSubmitting(false);
2146
+ }
2147
+ };
2148
+ const changeRole = async (userId, role) => {
2149
+ if (!tenantId) return;
2150
+ try {
2151
+ await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}/users/${userId}/role`, {
2152
+ method: "PATCH",
2153
+ headers: { "Content-Type": "application/json" },
2154
+ credentials: "include",
2155
+ body: JSON.stringify({ role })
2156
+ });
2157
+ await reloadMembers();
2158
+ setActionMessage("Role updated.");
2159
+ } catch (err) {
2160
+ setError(err instanceof Error ? err.message : "Failed to update role");
2161
+ }
2162
+ };
2163
+ const removeMember = async (userId) => {
2164
+ if (!tenantId) return;
2165
+ try {
2166
+ await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}/users/${userId}`, { method: "DELETE", credentials: "include" });
2167
+ await reloadMembers();
2168
+ setActionMessage("Member removed.");
2169
+ } catch (err) {
2170
+ setError(err instanceof Error ? err.message : "Failed to remove member");
2171
+ }
2172
+ };
2173
+ const resendInvite = async (id) => {
2174
+ if (!tenantId) return;
2175
+ try {
2176
+ await jsonFetch(`${baseUrl}/api/v1/invites/${id}/resend`, { method: "POST", credentials: "include" });
2177
+ setActionMessage("Invitation resent.");
2178
+ await loadInvites(tenantId);
2179
+ } catch (err) {
2180
+ setError(err instanceof Error ? err.message : "Failed to resend invitation");
2181
+ }
2182
+ };
2183
+ const revokeInvite = async (id) => {
2184
+ if (!tenantId) return;
2185
+ try {
2186
+ await jsonFetch(`${baseUrl}/api/v1/invites/${id}/revoke`, { method: "POST", credentials: "include" });
2187
+ setActionMessage("Invitation revoked.");
2188
+ await loadInvites(tenantId);
2189
+ } catch (err) {
2190
+ setError(err instanceof Error ? err.message : "Failed to revoke invitation");
2191
+ }
2192
+ };
2193
+ const submitDelete = async () => {
2194
+ if (!tenantId || !tenant) return;
2195
+ if (confirmDeleteText !== tenant.slug) {
2196
+ setError("Type the organization slug to confirm deletion.");
2197
+ return;
2198
+ }
2199
+ if (!confirmDeletePassword) {
2200
+ setError("Re-enter your password to confirm deletion.");
2201
+ return;
2202
+ }
2203
+ setDeleteSubmitting(true);
2204
+ setError(null);
2205
+ try {
2206
+ await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}`, {
2207
+ method: "DELETE",
2208
+ credentials: "include",
2209
+ headers: { "Content-Type": "application/json" },
2210
+ body: JSON.stringify({ confirmPassword: confirmDeletePassword })
2211
+ });
2212
+ setActionMessage("Organization deleted.");
2213
+ setConfirmDeletePassword("");
2214
+ onDeleted?.(tenantId);
2215
+ } catch (err) {
2216
+ setError(err instanceof Error ? err.message : "Failed to delete organization");
2217
+ } finally {
2218
+ setDeleteSubmitting(false);
2219
+ }
2220
+ };
2221
+ if (loading) {
2222
+ return /* @__PURE__ */ jsx(Shell, { appearance, branding, className, title: "Organization", children: /* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", style: { fontSize: 13 }, children: "Loading\u2026" }) });
2223
+ }
2224
+ const tabBtnStyle = (key) => ({
2225
+ background: activeTab === key ? `${branding?.accentColor || "#6366f1"}1a` : "transparent",
2226
+ border: `1px solid ${activeTab === key ? branding?.accentColor || "#6366f1" : "rgba(15,23,42,0.12)"}`,
2227
+ color: branding?.primaryColor || "#0f172a",
2228
+ padding: "6px 12px",
2229
+ borderRadius: 6,
2230
+ cursor: "pointer",
2231
+ fontSize: 12,
2232
+ fontWeight: 500
2233
+ });
2234
+ return /* @__PURE__ */ jsx(Shell, { appearance, branding, className, title: tenant?.name || "Organization", subtitle: tenant?.slug ? `slug: ${tenant.slug}` : void 0, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, "data-iqauth-sdk-org-profile": "", children: [
2235
+ error ? /* @__PURE__ */ jsx(ErrorBanner, { message: error }) : null,
2236
+ actionMessage ? /* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", style: { fontSize: 13, color: "#047857", margin: 0 }, children: actionMessage }) : null,
2237
+ /* @__PURE__ */ jsx("div", { role: "tablist", "aria-label": "Organization sections", style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: visibleTabsFiltered.map((t2) => /* @__PURE__ */ jsx(
2238
+ "button",
2239
+ {
2240
+ role: "tab",
2241
+ "aria-selected": activeTab === t2,
2242
+ "aria-controls": `iqauth-org-tab-${t2}`,
2243
+ "data-testid": `tab-org-${t2}`,
2244
+ type: "button",
2245
+ onClick: () => setActiveTab(t2),
2246
+ style: tabBtnStyle(t2),
2247
+ children: t2 === "general" ? "General" : t2 === "members" ? `Members (${members.length})` : t2 === "invitations" ? "Invitations" : "Danger zone"
2248
+ },
2249
+ t2
2250
+ )) }),
2251
+ activeTab === "general" ? /* @__PURE__ */ jsxs("section", { role: "tabpanel", id: "iqauth-org-tab-general", "aria-labelledby": "tab-org-general", style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
2252
+ /* @__PURE__ */ jsx("h3", { style: { fontSize: 14, fontWeight: 600, margin: 0 }, children: "Settings" }),
2253
+ /* @__PURE__ */ jsxs("form", { onSubmit: submitRename, style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
2254
+ /* @__PURE__ */ jsx(Field, { label: "Organization name", children: /* @__PURE__ */ jsx("input", { "data-testid": "input-org-rename", style: inputStyle(), value: renameValue, onChange: (e) => setRenameValue(e.target.value), required: true, minLength: 2 }) }),
2255
+ /* @__PURE__ */ jsx(Field, { label: "Slug", children: /* @__PURE__ */ jsx("input", { "data-testid": "input-org-slug", style: { ...inputStyle(), fontFamily: "monospace" }, value: slugValue, onChange: (e) => setSlugValue(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-")), required: true, pattern: "[a-z0-9-]+", minLength: 2 }) }),
2256
+ /* @__PURE__ */ jsx(PrimaryButton, { "data-testid": "button-org-rename", type: "submit", disabled: renameSubmitting || renameValue.trim() === tenant?.name && slugValue.trim() === tenant?.slug, children: renameSubmitting ? "Saving\u2026" : "Save changes" })
2257
+ ] })
2258
+ ] }) : null,
2259
+ activeTab === "members" ? /* @__PURE__ */ jsxs("section", { role: "tabpanel", id: "iqauth-org-tab-members", "aria-labelledby": "tab-org-members", style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
2260
+ /* @__PURE__ */ jsxs("form", { onSubmit: submitInvite, style: { display: "flex", gap: 8, flexWrap: "wrap" }, "aria-label": "Invite a new member", children: [
2261
+ /* @__PURE__ */ jsx("input", { "data-testid": "input-org-invite-email", type: "email", placeholder: "email@company.com", "aria-label": "Email", style: { ...inputStyle(), flex: 1, minWidth: 180 }, value: inviteEmail, onChange: (e) => setInviteEmail(e.target.value), required: true }),
2262
+ /* @__PURE__ */ jsxs("select", { "data-testid": "select-org-invite-role", "aria-label": "Role", style: { ...inputStyle(), width: "auto" }, value: inviteRole, onChange: (e) => setInviteRole(e.target.value), children: [
2263
+ /* @__PURE__ */ jsx("option", { value: "tenant_member", children: "tenant_member" }),
2264
+ /* @__PURE__ */ jsx("option", { value: "tenant_admin", children: "tenant_admin" })
2265
+ ] }),
2266
+ /* @__PURE__ */ jsx(PrimaryButton, { "data-testid": "button-org-invite", type: "submit", disabled: inviteSubmitting || !inviteEmail.trim(), children: inviteSubmitting ? "Sending\u2026" : "Send invite" })
2267
+ ] }),
2268
+ members.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, opacity: 0.6 }, children: "No members yet." }) : /* @__PURE__ */ jsx("ul", { "aria-label": "Members", style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: members.map((m) => /* @__PURE__ */ jsxs("li", { "data-testid": `row-org-member-${m.userId}`, style: { display: "grid", gridTemplateColumns: "1fr auto auto", gap: 8, alignItems: "center", padding: "8px 10px", background: "rgba(15,23,42,0.04)", borderRadius: 6, fontSize: 13 }, children: [
2269
+ /* @__PURE__ */ jsxs("div", { children: [
2270
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500 }, children: m.name || m.email }),
2271
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: m.email })
2272
+ ] }),
2273
+ /* @__PURE__ */ jsxs("select", { "data-testid": `select-org-member-role-${m.userId}`, "aria-label": `Role for ${m.email}`, value: m.role, onChange: (e) => changeRole(m.userId, e.target.value), style: { ...inputStyle(), width: "auto", padding: "4px 8px", fontSize: 12 }, children: [
2274
+ /* @__PURE__ */ jsx("option", { value: "tenant_member", children: "tenant_member" }),
2275
+ /* @__PURE__ */ jsx("option", { value: "tenant_admin", children: "tenant_admin" })
2276
+ ] }),
2277
+ /* @__PURE__ */ jsx(
2278
+ "button",
2279
+ {
2280
+ type: "button",
2281
+ "data-testid": `button-org-member-remove-${m.userId}`,
2282
+ "aria-label": `Remove ${m.email}`,
2283
+ onClick: () => removeMember(m.userId),
2284
+ style: { background: "transparent", border: "1px solid rgba(220,38,38,0.4)", color: "#b91c1c", borderRadius: 4, padding: "4px 10px", cursor: "pointer", fontSize: 12 },
2285
+ children: "Remove"
2286
+ }
2287
+ )
2288
+ ] }, m.userId)) })
2289
+ ] }) : null,
2290
+ activeTab === "invitations" ? /* @__PURE__ */ jsxs("section", { role: "tabpanel", id: "iqauth-org-tab-invitations", "aria-labelledby": "tab-org-invitations", style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
2291
+ /* @__PURE__ */ jsx("h3", { style: { fontSize: 14, fontWeight: 600, margin: 0 }, children: "Pending invitations" }),
2292
+ invitesLoading ? /* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", style: { fontSize: 13 }, children: "Loading\u2026" }) : pendingInvites.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, opacity: 0.6 }, children: "No pending invitations." }) : /* @__PURE__ */ jsx("ul", { "aria-label": "Pending invitations", style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: pendingInvites.map((inv) => /* @__PURE__ */ jsxs("li", { "data-testid": `row-org-invite-${inv.id}`, style: { display: "grid", gridTemplateColumns: "1fr auto auto", gap: 8, alignItems: "center", padding: "8px 10px", background: "rgba(15,23,42,0.04)", borderRadius: 6, fontSize: 13 }, children: [
2293
+ /* @__PURE__ */ jsxs("div", { children: [
2294
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500 }, children: inv.email }),
2295
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: [
2296
+ "role: ",
2297
+ inv.role,
2298
+ inv.expiresAt ? ` \u2022 expires ${new Date(inv.expiresAt).toLocaleDateString()}` : ""
2299
+ ] })
2300
+ ] }),
2301
+ /* @__PURE__ */ jsx(
2302
+ "button",
2303
+ {
2304
+ type: "button",
2305
+ "data-testid": `button-org-invite-resend-${inv.id}`,
2306
+ onClick: () => resendInvite(inv.id),
2307
+ style: { background: "transparent", border: "1px solid rgba(15,23,42,0.18)", color: "#0f172a", borderRadius: 4, padding: "4px 10px", cursor: "pointer", fontSize: 12 },
2308
+ children: "Resend"
2309
+ }
2310
+ ),
2311
+ /* @__PURE__ */ jsx(
2312
+ "button",
2313
+ {
2314
+ type: "button",
2315
+ "data-testid": `button-org-invite-revoke-${inv.id}`,
2316
+ onClick: () => revokeInvite(inv.id),
2317
+ style: { background: "transparent", border: "1px solid rgba(220,38,38,0.4)", color: "#b91c1c", borderRadius: 4, padding: "4px 10px", cursor: "pointer", fontSize: 12 },
2318
+ children: "Revoke"
2319
+ }
2320
+ )
2321
+ ] }, inv.id)) })
2322
+ ] }) : null,
2323
+ activeTab === "danger" ? /* @__PURE__ */ jsxs("section", { role: "tabpanel", id: "iqauth-org-tab-danger", "aria-labelledby": "tab-org-danger", style: { display: "flex", flexDirection: "column", gap: 10, border: "1px solid rgba(220,38,38,0.3)", padding: 12, borderRadius: 8, background: "rgba(220,38,38,0.04)" }, children: [
2324
+ /* @__PURE__ */ jsx("h3", { style: { fontSize: 14, fontWeight: 600, margin: 0, color: "#b91c1c" }, children: "Delete organization" }),
2325
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, opacity: 0.85, margin: 0 }, children: [
2326
+ "This permanently deletes ",
2327
+ /* @__PURE__ */ jsx("strong", { children: tenant?.name }),
2328
+ " and all of its members, roles, and audit history. To confirm, type the slug ",
2329
+ /* @__PURE__ */ jsx("code", { children: tenant?.slug }),
2330
+ " below."
2331
+ ] }),
2332
+ /* @__PURE__ */ jsx(Field, { label: "Type the organization slug to confirm", children: /* @__PURE__ */ jsx("input", { "data-testid": "input-org-delete-confirm", autoComplete: "off", placeholder: tenant?.slug || "", "aria-label": "Type slug to confirm", style: { ...inputStyle(), fontFamily: "monospace" }, value: confirmDeleteText, onChange: (e) => setConfirmDeleteText(e.target.value) }) }),
2333
+ /* @__PURE__ */ jsx(Field, { label: "Re-enter your password", children: /* @__PURE__ */ jsx("input", { "data-testid": "input-org-delete-password", type: "password", autoComplete: "current-password", "aria-label": "Password to confirm deletion", style: inputStyle(), value: confirmDeletePassword, onChange: (e) => setConfirmDeletePassword(e.target.value) }) }),
2334
+ /* @__PURE__ */ jsx(
2335
+ "button",
2336
+ {
2337
+ type: "button",
2338
+ "data-testid": "button-org-delete",
2339
+ onClick: submitDelete,
2340
+ disabled: deleteSubmitting || confirmDeleteText !== tenant?.slug || !confirmDeletePassword,
2341
+ style: { background: "#b91c1c", color: "#fff", border: "none", padding: "8px 14px", borderRadius: 6, cursor: confirmDeleteText === tenant?.slug && confirmDeletePassword ? "pointer" : "not-allowed", fontSize: 13, opacity: confirmDeleteText === tenant?.slug && confirmDeletePassword ? 1 : 0.5 },
2342
+ children: deleteSubmitting ? "Deleting\u2026" : "Permanently delete organization"
2343
+ }
2344
+ )
2345
+ ] }) : null
2346
+ ] }) });
2347
+ }
2348
+ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRedirectUrl, appearance, className }) {
2349
+ const [showCreateForm, setShowCreateForm] = useState(false);
2350
+ const reloadList = () => {
2351
+ setShowCreateForm(false);
2352
+ if (typeof window !== "undefined") setTimeout(() => window.location.reload(), 50);
2353
+ };
2354
+ const branding = useResolvedSdkBranding(iqAuthBaseUrl);
2355
+ const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
2356
+ const accent = branding?.accentColor || "#6366f1";
2357
+ const [memberships, setMemberships] = useState([]);
2358
+ const [activeTenantId, setActiveTenantId] = useState(null);
2359
+ const [loading, setLoading] = useState(true);
2360
+ const [error, setError] = useState(null);
2361
+ useEffect(() => {
2362
+ let cancelled = false;
2363
+ Promise.all([
2364
+ fetch(`${baseUrl}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()),
2365
+ fetch(`${baseUrl}/api/v1/tenants/memberships`, { credentials: "include" }).then((r) => r.json())
2366
+ ]).then(([me, mems]) => {
2367
+ if (cancelled) return;
2368
+ setActiveTenantId(me?.data?.tenantId || null);
2369
+ setMemberships(mems?.data?.memberships || mems?.data || []);
2370
+ }).catch((err) => {
2371
+ if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load organizations");
2372
+ }).finally(() => {
2373
+ if (!cancelled) setLoading(false);
2374
+ });
2375
+ return () => {
2376
+ cancelled = true;
2377
+ };
2378
+ }, [baseUrl]);
2379
+ const select = async (tenantId) => {
2380
+ if (tenantId === activeTenantId) {
2381
+ onSelect?.(tenantId);
2382
+ return;
2383
+ }
2384
+ try {
2385
+ await jsonFetch(`${baseUrl}/api/v1/auth/select-tenant`, {
2386
+ method: "POST",
2387
+ headers: { "Content-Type": "application/json" },
2388
+ credentials: "include",
2389
+ body: JSON.stringify({ tenantId })
2390
+ });
2391
+ setActiveTenantId(tenantId);
2392
+ onSelect?.(tenantId);
2393
+ } catch (err) {
2394
+ setError(err instanceof Error ? err.message : "Failed to switch organization");
2395
+ }
2396
+ };
2397
+ return /* @__PURE__ */ jsx(Shell, { appearance, branding, className, title: "Your organizations", subtitle: "Select an organization to make it active.", children: /* @__PURE__ */ jsxs("div", { "data-iqauth-sdk-org-list": "", style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
2398
+ error ? /* @__PURE__ */ jsx(ErrorBanner, { message: error }) : null,
2399
+ loading ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13 }, children: "Loading\u2026" }) : memberships.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, opacity: 0.6 }, children: "You don\u2019t belong to any organizations yet." }) : /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: memberships.map((m) => {
2400
+ const active = m.tenantId === activeTenantId;
2401
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
2402
+ "button",
2403
+ {
2404
+ type: "button",
2405
+ "data-testid": `button-org-list-${m.tenantId}`,
2406
+ onClick: () => select(m.tenantId),
2407
+ style: {
2408
+ display: "block",
2409
+ width: "100%",
2410
+ textAlign: "left",
2411
+ padding: "10px 12px",
2412
+ borderRadius: 6,
2413
+ cursor: "pointer",
2414
+ fontSize: 13,
2415
+ background: active ? `${accent}1a` : "transparent",
2416
+ border: `1px solid ${active ? accent : "rgba(15,23,42,0.12)"}`,
2417
+ color: branding?.primaryColor || "#0f172a"
2418
+ },
2419
+ children: [
2420
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
2421
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: [
2422
+ (m.roles || []).join(", ") || "\u2014",
2423
+ active ? /* @__PURE__ */ jsx("span", { style: { marginLeft: 8, color: accent, fontWeight: 600 }, children: "active" }) : null
2424
+ ] })
2425
+ ]
2426
+ }
2427
+ ) }, m.tenantId);
2428
+ }) }),
2429
+ showCreate ? /* @__PURE__ */ jsx("div", { style: { marginTop: 12, paddingTop: 12, borderTop: "1px solid rgba(15,23,42,0.08)" }, children: showCreateForm ? /* @__PURE__ */ jsxs(Fragment2, { children: [
2430
+ /* @__PURE__ */ jsx(
2431
+ CreateOrganization,
2432
+ {
2433
+ iqAuthBaseUrl,
2434
+ unstyled: true,
2435
+ appearance,
2436
+ onCreated: (t2) => {
2437
+ onSelect?.(t2.id);
2438
+ reloadList();
2439
+ },
2440
+ redirectUrl: createRedirectUrl
2441
+ }
2442
+ ),
2443
+ /* @__PURE__ */ jsx(
2444
+ "button",
2445
+ {
2446
+ type: "button",
2447
+ "data-testid": "button-org-list-create-cancel",
2448
+ onClick: () => setShowCreateForm(false),
2449
+ style: { marginTop: 8, background: "transparent", border: "none", color: branding?.accentColor || "#6366f1", cursor: "pointer", fontSize: 12, padding: 0 },
2450
+ children: "Cancel"
2451
+ }
2452
+ )
2453
+ ] }) : /* @__PURE__ */ jsx(
2454
+ "button",
2455
+ {
2456
+ type: "button",
2457
+ "data-testid": "button-org-list-create",
2458
+ onClick: () => setShowCreateForm(true),
2459
+ style: { background: "transparent", border: `1px dashed ${accent}`, color: branding?.primaryColor || "#0f172a", padding: "10px 12px", borderRadius: 6, cursor: "pointer", fontSize: 13, width: "100%" },
2460
+ children: "+ Create new organization"
2461
+ }
2462
+ ) }) : null
2463
+ ] }) });
2464
+ }
2465
+ function Waitlist({ iqAuthBaseUrl, appKey, appId, title, subtitle, successMessage, appearance, className }) {
2466
+ const branding = useResolvedSdkBranding(iqAuthBaseUrl, appId);
2467
+ const [email, setEmail] = useState("");
2468
+ const [name, setName] = useState("");
2469
+ const [organizationName, setOrganizationName] = useState("");
2470
+ const [submitting, setSubmitting] = useState(false);
2471
+ const [error, setError] = useState(null);
2472
+ const [submitted, setSubmitted] = useState(null);
2473
+ const submit = async (e) => {
2474
+ e.preventDefault();
2475
+ setSubmitting(true);
2476
+ setError(null);
2477
+ try {
2478
+ const res = await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/waitlist`, {
2479
+ method: "POST",
2480
+ headers: { "Content-Type": "application/json" },
2481
+ body: JSON.stringify({
2482
+ email: email.trim().toLowerCase(),
2483
+ name: name.trim() || void 0,
2484
+ organizationName: organizationName.trim() || void 0,
2485
+ appKey: appKey || void 0,
2486
+ appId: appId || void 0
2487
+ })
2488
+ });
2489
+ const data = res?.data ?? res;
2490
+ setSubmitted({ duplicate: !!data?.duplicate });
2491
+ } catch (err) {
2492
+ setError(err instanceof Error ? err.message : "Failed to join the waitlist");
2493
+ } finally {
2494
+ setSubmitting(false);
2495
+ }
2496
+ };
2497
+ if (submitted) {
2498
+ return /* @__PURE__ */ jsx(Shell, { appearance, branding, className, title: title || "You\u2019re on the list", children: /* @__PURE__ */ jsx("p", { "data-testid": "text-waitlist-success", style: { fontSize: 14 }, children: successMessage || (submitted.duplicate ? "You were already on the waitlist \u2014 we\u2019ll be in touch." : "Thanks! We\u2019ll email you when access opens up.") }) });
2499
+ }
2500
+ return /* @__PURE__ */ jsx(Shell, { appearance, branding, className, title: title || "Join the waitlist", subtitle: subtitle || "Enter your email and we\u2019ll be in touch when access opens up.", children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, style: { display: "flex", flexDirection: "column", gap: 12 }, "data-iqauth-sdk-waitlist": "", children: [
2501
+ error ? /* @__PURE__ */ jsx(ErrorBanner, { message: error }) : null,
2502
+ /* @__PURE__ */ jsx(Field, { label: "Work email", children: /* @__PURE__ */ jsx("input", { "data-testid": "input-waitlist-email", type: "email", autoComplete: "email", required: true, style: inputStyle(), value: email, onChange: (e) => setEmail(e.target.value) }) }),
2503
+ /* @__PURE__ */ jsx(Field, { label: "Name (optional)", children: /* @__PURE__ */ jsx("input", { "data-testid": "input-waitlist-name", autoComplete: "name", style: inputStyle(), value: name, onChange: (e) => setName(e.target.value) }) }),
2504
+ /* @__PURE__ */ jsx(Field, { label: "Organization (optional)", children: /* @__PURE__ */ jsx("input", { "data-testid": "input-waitlist-org", autoComplete: "organization", style: inputStyle(), value: organizationName, onChange: (e) => setOrganizationName(e.target.value) }) }),
2505
+ /* @__PURE__ */ jsx(PrimaryButton, { "data-testid": "button-waitlist-submit", type: "submit", disabled: submitting || !email, children: submitting ? "Joining\u2026" : "Join the waitlist" })
2506
+ ] }) });
2507
+ }
2508
+ function usePasswordlessOptions(override) {
2509
+ const ctx = useContext(IQAuthContext);
2510
+ const baseFromCtx = ctx?.manager?.issuerUrl;
2511
+ const iqAuthBaseUrl = override?.iqAuthBaseUrl || baseFromCtx || (typeof window !== "undefined" ? window.location.origin : "");
2512
+ return { iqAuthBaseUrl, cookieSession: override?.cookieSession ?? true };
2513
+ }
2514
+ function useMagicLink(override) {
2515
+ const opts = usePasswordlessOptions(override);
2516
+ const [sent, setSent] = useState(false);
2517
+ const [busy, setBusy] = useState(false);
2518
+ const [error, setError] = useState(null);
2519
+ const request = useCallback(async (input) => {
2520
+ setBusy(true);
2521
+ setError(null);
2522
+ setSent(false);
2523
+ try {
2524
+ await requestMagicLink(opts, input);
2525
+ setSent(true);
2526
+ } catch (e) {
2527
+ setError(e instanceof Error ? e.message : "Magic link request failed");
2528
+ } finally {
2529
+ setBusy(false);
2530
+ }
2531
+ }, [opts.iqAuthBaseUrl, opts.cookieSession]);
2532
+ return { request, sent, busy, error };
2533
+ }
2534
+ function usePasskey(override) {
2535
+ const opts = usePasswordlessOptions(override);
2536
+ const [busy, setBusy] = useState(false);
2537
+ const [error, setError] = useState(null);
2538
+ const signIn2 = useCallback(async (input = {}) => {
2539
+ setBusy(true);
2540
+ setError(null);
2541
+ try {
2542
+ return await signInWithPasskey(opts, input);
2543
+ } catch (e) {
2544
+ const msg = e instanceof Error ? e.message : "Passkey sign-in failed";
2545
+ setError(msg);
2546
+ throw e;
2547
+ } finally {
2548
+ setBusy(false);
2549
+ }
2550
+ }, [opts.iqAuthBaseUrl]);
2551
+ const enroll = useCallback(async (name) => {
2552
+ setBusy(true);
2553
+ setError(null);
2554
+ try {
2555
+ return await enrollPasskey(opts, name);
2556
+ } catch (e) {
2557
+ const msg = e instanceof Error ? e.message : "Passkey enrollment failed";
2558
+ setError(msg);
2559
+ throw e;
2560
+ } finally {
2561
+ setBusy(false);
2562
+ }
2563
+ }, [opts.iqAuthBaseUrl]);
2564
+ return { signIn: signIn2, enroll, busy, error };
2565
+ }
2566
+ function useLinkedIdentities(override) {
2567
+ const opts = usePasswordlessOptions(override);
2568
+ const [identities, setIdentities] = useState([]);
2569
+ const [loading, setLoading] = useState(true);
2570
+ const [error, setError] = useState(null);
2571
+ const refresh = useCallback(async () => {
2572
+ setLoading(true);
2573
+ setError(null);
2574
+ try {
2575
+ setIdentities(await listLinkedIdentities(opts));
2576
+ } catch (e) {
2577
+ setError(e instanceof Error ? e.message : "Failed to load identities");
2578
+ } finally {
2579
+ setLoading(false);
2580
+ }
2581
+ }, [opts.iqAuthBaseUrl]);
2582
+ const link = useCallback(async (input) => {
2583
+ await linkProvider(opts, input);
2584
+ await refresh();
2585
+ }, [opts.iqAuthBaseUrl, refresh]);
2586
+ const unlink = useCallback(async (provider, password) => {
2587
+ await unlinkProvider(opts, { provider, reauth: { password } });
2588
+ await refresh();
2589
+ }, [opts.iqAuthBaseUrl, refresh]);
2590
+ useEffect(() => {
2591
+ refresh();
2592
+ }, [refresh]);
2593
+ return { identities, loading, error, refresh, link, unlink };
2594
+ }
2595
+ function MagicLinkSignInForm(props) {
2596
+ const { request, sent, busy, error } = useMagicLink(props);
2597
+ const [email, setEmail] = useState("");
2598
+ return /* @__PURE__ */ jsxs(
2599
+ "form",
2600
+ {
2601
+ "data-testid": "form-magic-link",
2602
+ className: props.className,
2603
+ onSubmit: (e) => {
2604
+ e.preventDefault();
2605
+ if (email) void request({ email, appId: props.appId, redirectUri: props.redirectUri });
2606
+ },
2607
+ style: { display: "flex", flexDirection: "column", gap: 8 },
2608
+ children: [
2609
+ /* @__PURE__ */ jsx(
2610
+ "input",
2611
+ {
2612
+ "data-testid": "input-magic-link-email",
2613
+ type: "email",
2614
+ required: true,
2615
+ value: email,
2616
+ placeholder: props.placeholder ?? "you@example.com",
2617
+ onChange: (e) => setEmail(e.target.value),
2618
+ style: { padding: 8, border: "1px solid rgba(15,23,42,0.15)", borderRadius: 6 }
2619
+ }
2620
+ ),
2621
+ /* @__PURE__ */ jsx("button", { "data-testid": "button-magic-link-submit", type: "submit", disabled: busy || !email, children: busy ? "Sending\u2026" : props.buttonLabel ?? "Email me a sign-in link" }),
2622
+ sent ? /* @__PURE__ */ jsx("p", { "data-testid": "text-magic-link-sent", style: { fontSize: 12 }, children: "If the email is on file, a link is on the way." }) : null,
2623
+ error ? /* @__PURE__ */ jsx("p", { "data-testid": "text-magic-link-error", style: { fontSize: 12, color: "#b91c1c" }, children: error }) : null
2624
+ ]
2625
+ }
2626
+ );
2627
+ }
2628
+ function PasskeySignInButton({ email, className, children, ...rest }) {
2629
+ const { signIn: signIn2, busy, error } = usePasskey(rest);
2630
+ return /* @__PURE__ */ jsxs("div", { className, children: [
2631
+ /* @__PURE__ */ jsx(
2632
+ "button",
2633
+ {
2634
+ "data-testid": "button-passkey-signin",
2635
+ disabled: busy,
2636
+ onClick: () => void signIn2({ email }).catch(() => {
2637
+ }),
2638
+ style: { padding: "8px 12px", borderRadius: 6, border: "1px solid rgba(15,23,42,0.15)", background: "transparent", cursor: "pointer" },
2639
+ children: busy ? "Verifying\u2026" : children ?? "Sign in with a passkey"
2640
+ }
2641
+ ),
2642
+ error ? /* @__PURE__ */ jsx("p", { "data-testid": "text-passkey-error", style: { fontSize: 12, color: "#b91c1c", marginTop: 4 }, children: error }) : null
2643
+ ] });
2644
+ }
2645
+ function LinkedAccounts({ className, onChange, ...rest }) {
2646
+ const { identities, loading, error, unlink } = useLinkedIdentities(rest);
2647
+ return /* @__PURE__ */ jsx("div", { "data-testid": "section-linked-accounts", className, children: loading ? /* @__PURE__ */ jsx("p", { children: "Loading\u2026" }) : error ? /* @__PURE__ */ jsx("p", { style: { color: "#b91c1c" }, children: error }) : /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: identities.map((i) => /* @__PURE__ */ jsxs("li", { "data-testid": `row-identity-${i.provider}`, style: { display: "flex", justifyContent: "space-between", padding: "6px 0" }, children: [
2648
+ /* @__PURE__ */ jsxs("span", { children: [
2649
+ /* @__PURE__ */ jsx("strong", { style: { textTransform: "capitalize" }, children: i.provider }),
2650
+ " ",
2651
+ /* @__PURE__ */ jsx("span", { style: { opacity: 0.7 }, children: i.label || i.providerUserId || "" })
2652
+ ] }),
2653
+ i.canUnlink ? /* @__PURE__ */ jsx(
2654
+ "button",
2655
+ {
2656
+ "data-testid": `button-unlink-${i.provider}`,
2657
+ onClick: async () => {
2658
+ const pw = window.prompt("Confirm your password to unlink this identity") || void 0;
2659
+ try {
2660
+ await unlink(i.provider, pw);
2661
+ onChange?.();
2662
+ } catch {
2663
+ }
2664
+ },
2665
+ children: "Unlink"
2666
+ }
2667
+ ) : null
2668
+ ] }, i.id)) }) });
2669
+ }
2670
+ var __version__ = "phase-bc-1.0.0";
2671
+
2672
+ export {
2673
+ sanitizeReturnTo,
2674
+ isReturnToAllowed,
2675
+ IQAuthProvider,
2676
+ __useIQAuthInternal,
2677
+ useLocale,
2678
+ useT,
2679
+ useUser,
2680
+ useSession,
2681
+ useAuth,
2682
+ useOrganization,
2683
+ useAuthFetch,
2684
+ useSessionList,
2685
+ revokeSession,
2686
+ MultisessionAppSupport,
2687
+ useAccountList,
2688
+ useAccountSwitcher,
2689
+ SignedIn,
2690
+ SignedOut,
2691
+ IQAuthLoading,
2692
+ IQAuthLoaded,
2693
+ RedirectToSignIn,
2694
+ Protect,
2695
+ RedirectToSignedIn,
2696
+ useReturnTo,
2697
+ IQAuthReturnToBouncer,
2698
+ preflightReturnTo,
2699
+ AuthCallback,
2700
+ useIQAuthSignInContext,
2701
+ sanitizeBrandCss,
2702
+ useResolvedSdkBranding,
2703
+ isSilentSsoEligible,
2704
+ SignIn,
2705
+ SignUp,
2706
+ UserButton,
2707
+ UserProfile,
2708
+ OrganizationSwitcher,
2709
+ useImpersonation,
2710
+ ImpersonationBanner,
2711
+ useReverification,
2712
+ slugify,
2713
+ CreateOrganization,
2714
+ OrganizationProfile,
2715
+ OrganizationList,
2716
+ Waitlist,
2717
+ useMagicLink,
2718
+ usePasskey,
2719
+ useLinkedIdentities,
2720
+ MagicLinkSignInForm,
2721
+ PasskeySignInButton,
2722
+ LinkedAccounts,
2723
+ __version__
2724
+ };