@iqauth/sdk 2.7.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/browser-session.d.mts +3 -3
  2. package/dist/browser-session.d.ts +3 -3
  3. package/dist/browser-session.js +31 -5
  4. package/dist/browser-session.mjs +1 -1
  5. package/dist/browser.d.mts +3 -3
  6. package/dist/browser.d.ts +3 -3
  7. package/dist/browser.js +23 -3
  8. package/dist/browser.mjs +1 -1
  9. package/dist/{chunk-YVALAG3B.mjs → chunk-25SSYDIP.mjs} +1 -1
  10. package/dist/{chunk-RTJAIBXY.mjs → chunk-4V7FKOTG.mjs} +23 -3
  11. package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
  12. package/dist/chunk-JRDVUWAL.mjs +46 -0
  13. package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
  14. package/dist/{chunk-PMAFENVI.mjs → chunk-VYQ3ETCK.mjs} +27 -12
  15. package/dist/{chunk-RR2MGPTK.mjs → chunk-WHT6WKTY.mjs} +539 -83
  16. package/dist/{chunk-RUJXRTEW.mjs → chunk-WSH4SW7F.mjs} +122 -8
  17. package/dist/{chunk-JXQI62A7.mjs → chunk-ZLJPABB7.mjs} +31 -5
  18. package/dist/{client-BGFnBpfc.d.mts → client-D8L-PaWr.d.mts} +14 -4
  19. package/dist/{client-CDQ21LvW.d.ts → client-DkPL0EPZ.d.ts} +14 -4
  20. package/dist/{express-Piv2WhWM.d.ts → express-Budysq4h.d.ts} +2 -2
  21. package/dist/{express-CVNQEkOr.d.mts → express-DDTA3qV1.d.mts} +2 -2
  22. package/dist/express.d.mts +5 -5
  23. package/dist/express.d.ts +5 -5
  24. package/dist/express.js +217 -36
  25. package/dist/express.mjs +38 -26
  26. package/dist/fastify.d.mts +10 -2
  27. package/dist/fastify.d.ts +10 -2
  28. package/dist/fastify.js +260 -16
  29. package/dist/fastify.mjs +80 -5
  30. package/dist/hono.d.mts +10 -2
  31. package/dist/hono.d.ts +10 -2
  32. package/dist/hono.js +240 -16
  33. package/dist/hono.mjs +60 -5
  34. package/dist/{index-5KSZEnDe.d.ts → index-Cko-d5po.d.mts} +227 -5
  35. package/dist/{index-CKoZHAoc.d.mts → index-RNqwEcmY.d.ts} +227 -5
  36. package/dist/index.d.mts +5 -5
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +149 -26
  39. package/dist/index.mjs +5 -5
  40. package/dist/locales.d.mts +1 -1
  41. package/dist/locales.d.ts +1 -1
  42. package/dist/locales.js +36 -0
  43. package/dist/locales.mjs +1 -1
  44. package/dist/mobile.d.mts +3 -3
  45. package/dist/mobile.d.ts +3 -3
  46. package/dist/mobile.js +31 -5
  47. package/dist/mobile.mjs +1 -1
  48. package/dist/next.d.mts +10 -2
  49. package/dist/next.d.ts +10 -2
  50. package/dist/next.js +212 -11
  51. package/dist/next.mjs +62 -4
  52. package/dist/{provisioningBridge-M5G47LWO.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
  53. package/dist/{provisioningBridge-CGpMRie4.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
  54. package/dist/react-permissions.d.mts +4 -4
  55. package/dist/react-permissions.d.ts +4 -4
  56. package/dist/react-permissions.mjs +4 -3
  57. package/dist/react.d.mts +4 -4
  58. package/dist/react.d.ts +4 -4
  59. package/dist/react.js +570 -41
  60. package/dist/react.mjs +19 -5
  61. package/dist/server/handlers.d.mts +56 -5
  62. package/dist/server/handlers.d.ts +56 -5
  63. package/dist/server/handlers.js +123 -8
  64. package/dist/server/handlers.mjs +3 -1
  65. package/dist/server.d.mts +28 -8
  66. package/dist/server.d.ts +28 -8
  67. package/dist/server.js +176 -14
  68. package/dist/server.mjs +9 -4
  69. package/dist/service.d.mts +3 -3
  70. package/dist/service.d.ts +3 -3
  71. package/dist/service.js +31 -5
  72. package/dist/service.mjs +1 -1
  73. package/dist/{signIn-T-CZ6t6r.d.mts → signIn-CReqfXsh.d.mts} +18 -1
  74. package/dist/{signIn-BLFnz8SV.d.ts → signIn-Cfa1GTpO.d.ts} +18 -1
  75. package/dist/{tokens-Bqhmqq_R.d.ts → tokens-9F6ETrzk.d.ts} +1 -1
  76. package/dist/{tokens-CITeoG6P.d.mts → tokens-B06VtvUi.d.mts} +1 -1
  77. package/dist/{types-XOV9XPVi.d.mts → types-Bn8O-OEd.d.mts} +66 -2
  78. package/dist/{types-XOV9XPVi.d.ts → types-Bn8O-OEd.d.ts} +66 -2
  79. package/dist/{types-BdQ2lqfT.d.mts → types-DnU2LhXR.d.mts} +6 -0
  80. package/dist/{types-BdQ2lqfT.d.ts → types-DnU2LhXR.d.ts} +6 -0
  81. package/dist/webhooks.d.mts +22 -9
  82. package/dist/webhooks.d.ts +22 -9
  83. package/dist/webhooks.js +27 -12
  84. package/dist/webhooks.mjs +1 -1
  85. package/dist/ws.d.mts +2 -2
  86. package/dist/ws.d.ts +2 -2
  87. package/docs/guides/invitations.md +65 -0
  88. package/package.json +7 -2
package/dist/react.js CHANGED
@@ -821,6 +821,7 @@ __export(react_exports, {
821
821
  Protect: () => Protect,
822
822
  RedirectToSignIn: () => RedirectToSignIn,
823
823
  RedirectToSignedIn: () => RedirectToSignedIn,
824
+ ScopeSwitcher: () => ScopeSwitcher,
824
825
  SignIn: () => SignIn,
825
826
  SignUp: () => SignUp,
826
827
  SignedIn: () => SignedIn,
@@ -830,9 +831,13 @@ __export(react_exports, {
830
831
  Waitlist: () => Waitlist,
831
832
  __useIQAuthInternal: () => __useIQAuthInternal,
832
833
  __version__: () => __version__,
834
+ claimSatisfiesScope: () => claimSatisfiesScope,
833
835
  isReturnToAllowed: () => isReturnToAllowed,
834
836
  isSilentSsoEligible: () => isSilentSsoEligible,
837
+ performScopeSwitch: () => performScopeSwitch,
838
+ performTenantSwitch: () => performTenantSwitch,
835
839
  preflightReturnTo: () => preflightReturnTo,
840
+ resolveAfterSignInDestination: () => resolveAfterSignInDestination,
836
841
  revokeSession: () => revokeSession,
837
842
  sanitizeBrandCss: () => sanitizeBrandCss,
838
843
  sanitizeReturnTo: () => sanitizeReturnTo,
@@ -846,6 +851,7 @@ __export(react_exports, {
846
851
  useLinkedIdentities: () => useLinkedIdentities,
847
852
  useLocale: () => useLocale,
848
853
  useMagicLink: () => useMagicLink,
854
+ useMemberships: () => useMemberships,
849
855
  useOrganization: () => useOrganization,
850
856
  usePasskey: () => usePasskey,
851
857
  useResolvedSdkBranding: () => useResolvedSdkBranding,
@@ -980,7 +986,10 @@ function claimsToSessionUser(claims) {
980
986
  ...claims.email_verified !== void 0 ? { emailVerified: claims.email_verified } : {},
981
987
  ...claims.given_name !== void 0 ? { givenName: claims.given_name } : {},
982
988
  ...claims.family_name !== void 0 ? { familyName: claims.family_name } : {},
983
- ...claims.locale !== void 0 ? { locale: claims.locale } : {}
989
+ ...claims.locale !== void 0 ? { locale: claims.locale } : {},
990
+ // Task #171 — surface the active source/client scope when the token was
991
+ // minted scoped, so consumers reading useUser().user can branch on it.
992
+ ...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
984
993
  };
985
994
  }
986
995
  var EMPTY = {
@@ -1355,10 +1364,27 @@ var SessionManager = class {
1355
1364
  * session and notify subscribers and other tabs.
1356
1365
  */
1357
1366
  applyAccessToken(accessToken, refreshToken) {
1367
+ this.adoptAccessToken(accessToken, { refreshToken });
1368
+ }
1369
+ /**
1370
+ * Task #197 — Adopt an access token that the server has already minted
1371
+ * for us (e.g. from `POST /api/v1/auth/switch-scope`) without contacting
1372
+ * the issuer. Swaps the in-memory token, re-decodes claims, bumps
1373
+ * `version`, schedules proactive refresh, and broadcasts a
1374
+ * `session:update` to peer tabs.
1375
+ *
1376
+ * This is the safe path for any server endpoint that returns a fresh
1377
+ * access token in its JSON body: we want the new claims (scope, roles,
1378
+ * etc.) to take effect immediately, even if the refresh-cookie round-trip
1379
+ * would have failed (network blip, rate limit, signout race). When the
1380
+ * server also rotated the refresh token, pass it via
1381
+ * `opts.refreshToken` so the cookie stays aligned.
1382
+ */
1383
+ adoptAccessToken(accessToken, opts) {
1358
1384
  const claims = decodeClaims(accessToken);
1359
1385
  const user = claimsToSessionUser(claims);
1360
- if (refreshToken) {
1361
- void Promise.resolve(this.tokenStore.write(refreshToken, { claims }));
1386
+ if (opts?.refreshToken) {
1387
+ void Promise.resolve(this.tokenStore.write(opts.refreshToken, { claims }));
1362
1388
  }
1363
1389
  this.update({
1364
1390
  status: user ? "authenticated" : "unauthenticated",
@@ -1806,6 +1832,7 @@ var enUS = {
1806
1832
  "signIn.useDifferentAccount": "Use a different account",
1807
1833
  "signIn.selectTenant": "Choose an organization",
1808
1834
  "signIn.selectTenantSubtitle": "You belong to multiple organizations. Pick one to continue.",
1835
+ "signIn.selectScope": "Choose scope",
1809
1836
  "signIn.dividerOr": "or",
1810
1837
  "signIn.preparingExperience": "Preparing your sign-in experience.",
1811
1838
  "signIn.applicationUnavailable": "Application unavailable",
@@ -1894,6 +1921,11 @@ var enUS = {
1894
1921
  "orgSwitcher.createNew": "Create organization",
1895
1922
  "orgSwitcher.manage": "Manage organization",
1896
1923
  "orgSwitcher.noOrgs": "You don't belong to any organizations yet.",
1924
+ "orgSwitcher.mfaRequiredTitle": "Two-factor verification required",
1925
+ "orgSwitcher.mfaRequiredBody": "This organization requires an extra verification step. Continue in the hosted sign-in to finish switching.",
1926
+ "orgSwitcher.scopeSelectionRequiredTitle": "Choose what to access",
1927
+ "orgSwitcher.scopeSelectionRequiredBody": "This organization needs you to pick which workspace to open. Continue in the hosted sign-in to choose.",
1928
+ "orgSwitcher.continueInHostedSignIn": "Continue in hosted sign-in \u2192",
1897
1929
  "orgProfile.title": "Organization settings",
1898
1930
  "orgProfile.generalTab": "General",
1899
1931
  "orgProfile.membersTab": "Members",
@@ -2004,6 +2036,7 @@ function sanitizeReturnTo(input, options = {}) {
2004
2036
  if (!input || typeof input !== "string") return fallback;
2005
2037
  const trimmed = input.trim();
2006
2038
  if (!trimmed) return fallback;
2039
+ if (trimmed.includes("\\")) return fallback;
2007
2040
  if (trimmed.startsWith("//")) return fallback;
2008
2041
  if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
2009
2042
  return trimmed;
@@ -2489,6 +2522,65 @@ function RedirectToSignIn(props = {}) {
2489
2522
  }
2490
2523
  return null;
2491
2524
  }
2525
+ async function performScopeSwitch(manager, base, target) {
2526
+ const res = await manager.fetch(`${base}/api/v1/auth/switch-scope`, {
2527
+ method: "POST",
2528
+ headers: { "Content-Type": "application/json" },
2529
+ body: JSON.stringify({ scopeType: target.type, scopeId: target.id })
2530
+ });
2531
+ const body = await res.json().catch(() => ({}));
2532
+ if (!res.ok) {
2533
+ throw new Error(body?.error?.message || `HTTP ${res.status}`);
2534
+ }
2535
+ const newToken = body?.data?.accessToken;
2536
+ if (newToken) {
2537
+ manager.adoptAccessToken(newToken);
2538
+ void manager.refresh().catch(() => void 0);
2539
+ return;
2540
+ }
2541
+ const ok = await manager.refresh();
2542
+ if (!ok) {
2543
+ throw new Error("scope switch succeeded server-side but token refresh failed; reload to recover");
2544
+ }
2545
+ }
2546
+ async function performTenantSwitch(manager, base, tenantId) {
2547
+ const res = await manager.fetch(`${base.replace(/\/$/, "")}/api/v1/auth/select-tenant`, {
2548
+ method: "POST",
2549
+ headers: { "Content-Type": "application/json" },
2550
+ body: JSON.stringify({ tenantId })
2551
+ });
2552
+ const body = await res.json().catch(() => ({}));
2553
+ if (!res.ok) {
2554
+ throw new Error(body?.error?.message || `HTTP ${res.status}`);
2555
+ }
2556
+ if (body?.data?.mfaChallengeToken) {
2557
+ return {
2558
+ kind: "mfa_required",
2559
+ tenantId: body?.data?.tenantId || tenantId,
2560
+ mfaChallengeToken: body.data.mfaChallengeToken,
2561
+ availableMethods: Array.isArray(body.data.availableMethods) ? body.data.availableMethods : []
2562
+ };
2563
+ }
2564
+ if (body?.data?.type === "scope_selection" || body?.data?.scopeSelectionToken) {
2565
+ return {
2566
+ kind: "scope_selection_required",
2567
+ tenantId: body?.data?.tenantId || tenantId,
2568
+ scopeSelectionToken: body?.data?.scopeSelectionToken || "",
2569
+ scopes: Array.isArray(body?.data?.scopes) ? body.data.scopes : []
2570
+ };
2571
+ }
2572
+ const newToken = body?.data?.accessToken;
2573
+ if (newToken) {
2574
+ manager.adoptAccessToken(newToken);
2575
+ void manager.refresh().catch(() => void 0);
2576
+ return { kind: "ok", tenantId: body?.data?.user?.tenantId || tenantId };
2577
+ }
2578
+ const ok = await manager.refresh();
2579
+ if (!ok) {
2580
+ throw new Error("tenant switch succeeded server-side but token refresh failed; reload to recover");
2581
+ }
2582
+ return { kind: "ok", tenantId };
2583
+ }
2492
2584
  function asArray(v) {
2493
2585
  if (v == null) return [];
2494
2586
  return Array.isArray(v) ? v : [v];
@@ -2515,7 +2607,14 @@ function claimPermissions(c) {
2515
2607
  }
2516
2608
  return Array.from(out);
2517
2609
  }
2518
- function Protect({ role, permission, condition, fallback = null, children }) {
2610
+ function claimSatisfiesScope(claims, required) {
2611
+ if (!claims) return false;
2612
+ const ctx = claims.scopeContext;
2613
+ if (!ctx || !ctx.type || !ctx.id) return false;
2614
+ const list = Array.isArray(required) ? required : [required];
2615
+ return list.some((r) => r.type === ctx.type && r.id === ctx.id);
2616
+ }
2617
+ function Protect({ role, permission, scope, condition, fallback = null, children }) {
2519
2618
  const { snapshot } = useCtx();
2520
2619
  if (snapshot.status !== "authenticated") return (0, import_react.createElement)(import_react.Fragment, null, fallback);
2521
2620
  const wantedRoles = asArray(role);
@@ -2528,6 +2627,9 @@ function Protect({ role, permission, condition, fallback = null, children }) {
2528
2627
  const have = new Set(claimPermissions(snapshot.claims));
2529
2628
  if (!wantedPerms.some((p) => have.has(p))) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
2530
2629
  }
2630
+ if (scope) {
2631
+ if (!claimSatisfiesScope(snapshot.claims, scope)) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
2632
+ }
2531
2633
  if (condition && !condition(snapshot.claims)) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
2532
2634
  return (0, import_react.createElement)(import_react.Fragment, null, children);
2533
2635
  }
@@ -3084,12 +3186,54 @@ function isSilentSsoEligible(ctx, effectivePrompt) {
3084
3186
  if (!ctx.returnAllowed) return false;
3085
3187
  return true;
3086
3188
  }
3189
+ function resolveAfterSignInDestination(args) {
3190
+ let raw = args.prop ?? null;
3191
+ if (!raw && typeof args.search === "string") {
3192
+ try {
3193
+ const params = new URLSearchParams(args.search);
3194
+ raw = params.get("return_to") ?? params.get("next");
3195
+ } catch {
3196
+ }
3197
+ }
3198
+ return sanitizeReturnTo(raw, {
3199
+ allowedOrigins: args.allowedOrigins,
3200
+ currentOrigin: args.currentOrigin,
3201
+ fallback: "/"
3202
+ });
3203
+ }
3087
3204
  function SignIn(props) {
3088
3205
  const providerCtx = (0, import_react.useContext)(IQAuthContext);
3089
3206
  const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.hostedIssuerUrl ?? "";
3090
3207
  const appKey = props.appKey ?? providerCtx?.manager.appKey ?? "";
3091
3208
  const returnTo = props.returnTo ?? (typeof window !== "undefined" ? `${window.location.origin}/api/iqauth/callback` : "");
3092
- const { onRedirect, className, prompt, appearance: instanceAppearance, silentSso: instanceSilentSso } = props;
3209
+ const afterSignInUrl = resolveAfterSignInDestination({
3210
+ prop: props.afterSignInUrl,
3211
+ search: typeof window !== "undefined" ? window.location.search : "",
3212
+ allowedOrigins: providerCtx?.allowedReturnOrigins
3213
+ });
3214
+ const { onRedirect, className, prompt, appearance: instanceAppearance, silentSso: instanceSilentSso, scopeHint: scopeHintProp } = props;
3215
+ const effectiveScopeHint = (() => {
3216
+ const fromProp = scopeHintProp ?? null;
3217
+ let raw = fromProp;
3218
+ if (!raw && typeof window !== "undefined") {
3219
+ try {
3220
+ raw = new URLSearchParams(window.location.search).get("scope_hint");
3221
+ } catch {
3222
+ }
3223
+ }
3224
+ if (!raw) return null;
3225
+ if (typeof raw === "object" && raw && raw.type && raw.id) {
3226
+ const t3 = raw.type;
3227
+ if (t3 === "vendor" || t3 === "source" || t3 === "client") return { type: t3, id: String(raw.id) };
3228
+ return null;
3229
+ }
3230
+ if (typeof raw === "string" && raw.includes(":")) {
3231
+ const [t3, id] = raw.split(":", 2);
3232
+ if ((t3 === "vendor" || t3 === "source" || t3 === "client") && id) return { type: t3, id };
3233
+ }
3234
+ return null;
3235
+ })();
3236
+ const scopeHintBody = effectiveScopeHint ? { scopeHint: effectiveScopeHint } : {};
3093
3237
  const silentSsoEnabled = instanceSilentSso ?? providerCtx?.silentSso ?? false;
3094
3238
  const appearance = instanceAppearance && providerCtx?.appearance ? { elements: { ...providerCtx.appearance.elements, ...instanceAppearance.elements } } : instanceAppearance ?? providerCtx?.appearance ?? null;
3095
3239
  if (!iqAuthBaseUrl || !appKey) {
@@ -3110,6 +3254,11 @@ function SignIn(props) {
3110
3254
  appKey
3111
3255
  });
3112
3256
  if (guardError) throw guardError;
3257
+ (0, import_react.useEffect)(() => {
3258
+ if (typeof document === "undefined") return;
3259
+ const attrs = "; path=/; SameSite=Lax" + (typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "");
3260
+ document.cookie = `iqauth_return_to=${encodeURIComponent(afterSignInUrl)}${attrs}`;
3261
+ }, [afterSignInUrl]);
3113
3262
  const preflightLoggedRef = (0, import_react.useRef)(false);
3114
3263
  (0, import_react.useEffect)(() => {
3115
3264
  if (!ctx || preflightLoggedRef.current) return;
@@ -3125,6 +3274,7 @@ function SignIn(props) {
3125
3274
  const [formError, setFormError] = (0, import_react.useState)("");
3126
3275
  const [mfa, setMfa] = (0, import_react.useState)(null);
3127
3276
  const [tenantSel, setTenantSel] = (0, import_react.useState)(null);
3277
+ const [scopeSel, setScopeSel] = (0, import_react.useState)(null);
3128
3278
  const [oauthExchanging, setOauthExchanging] = (0, import_react.useState)(false);
3129
3279
  const [silent, setSilent] = (0, import_react.useState)("idle");
3130
3280
  const [forcePrompt, setForcePrompt] = (0, import_react.useState)(false);
@@ -3155,6 +3305,12 @@ function SignIn(props) {
3155
3305
  setTenantSel({ token: payload.tenantSelectionToken, tenants: payload.tenants || [] });
3156
3306
  return true;
3157
3307
  }
3308
+ if (payload.type === "scope_selection") {
3309
+ setScopeSel({ token: payload.scopeSelectionToken, tenantId: payload.tenantId, scopes: payload.scopes || [] });
3310
+ setTenantSel(null);
3311
+ setMfa(null);
3312
+ return true;
3313
+ }
3158
3314
  if (payload.type === "mfa_required") {
3159
3315
  const methods = payload.availableMethods || ["totp"];
3160
3316
  setMfa({ token: payload.mfaChallengeToken, methods, selected: methods[0], code: "", backup: false });
@@ -3175,7 +3331,7 @@ function SignIn(props) {
3175
3331
  method: "POST",
3176
3332
  headers: { "Content-Type": "application/json" },
3177
3333
  credentials: "include",
3178
- body: JSON.stringify({ email, password, ...oidcPayload() })
3334
+ body: JSON.stringify({ email, password, ...oidcPayload(), ...scopeHintBody })
3179
3335
  });
3180
3336
  const payload = await r.json().catch(() => ({}));
3181
3337
  if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
@@ -3198,7 +3354,8 @@ function SignIn(props) {
3198
3354
  code: mfa.code,
3199
3355
  method: mfa.selected,
3200
3356
  useBackup: mfa.backup,
3201
- ...oidcPayload()
3357
+ ...oidcPayload(),
3358
+ ...scopeHintBody
3202
3359
  })
3203
3360
  });
3204
3361
  const payload = await r.json().catch(() => ({}));
@@ -3213,7 +3370,21 @@ function SignIn(props) {
3213
3370
  method: "POST",
3214
3371
  headers: { "Content-Type": "application/json" },
3215
3372
  credentials: "include",
3216
- body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload() })
3373
+ body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload(), ...scopeHintBody })
3374
+ });
3375
+ const payload = await r.json().catch(() => ({}));
3376
+ if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
3377
+ setSubmitting(false);
3378
+ };
3379
+ const submitScope = async (membershipId) => {
3380
+ if (!scopeSel) return;
3381
+ setSubmitting(true);
3382
+ setFormError("");
3383
+ const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-scope-select`, {
3384
+ method: "POST",
3385
+ headers: { "Content-Type": "application/json" },
3386
+ credentials: "include",
3387
+ body: JSON.stringify({ scopeSelectionToken: scopeSel.token, membershipId, ...oidcPayload(), ...scopeHintBody })
3217
3388
  });
3218
3389
  const payload = await r.json().catch(() => ({}));
3219
3390
  if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
@@ -3281,6 +3452,11 @@ function SignIn(props) {
3281
3452
  setSilent("failed");
3282
3453
  return;
3283
3454
  }
3455
+ if (payload?.type === "scope_selection") {
3456
+ setScopeSel({ token: payload.scopeSelectionToken, tenantId: payload.tenantId, scopes: payload.scopes || [] });
3457
+ setSilent("failed");
3458
+ return;
3459
+ }
3284
3460
  setSilent("failed");
3285
3461
  } catch {
3286
3462
  setSilent("failed");
@@ -3292,6 +3468,7 @@ function SignIn(props) {
3292
3468
  setForcePrompt(true);
3293
3469
  setSilent("skipped");
3294
3470
  setTenantSel(null);
3471
+ setScopeSel(null);
3295
3472
  if (typeof window !== "undefined") {
3296
3473
  try {
3297
3474
  const u = new URL(window.location.href);
@@ -3362,7 +3539,27 @@ function SignIn(props) {
3362
3539
  ]
3363
3540
  },
3364
3541
  tn.tenantId
3365
- )) }) : mfa ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("mfa.title"), children: [
3542
+ )) }) : scopeSel ? (
3543
+ // Task #171 — scope picker (source/client memberships)
3544
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-label": t2("signIn.selectScope") || "Choose scope", "data-iqauth-scope-picker": "1", style: { display: "flex", flexDirection: "column", gap: 8 }, children: scopeSel.scopes.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3545
+ "button",
3546
+ {
3547
+ type: "button",
3548
+ "data-iqauth-scope": s.membershipId,
3549
+ onClick: () => submitScope(s.membershipId),
3550
+ style: { textAlign: "left", padding: "10px 14px", border: "1px solid rgba(15,23,42,0.15)", borderRadius: 8, background: "transparent", color: "inherit", cursor: "pointer" },
3551
+ children: [
3552
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: s.scopeName }),
3553
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { margin: 0, fontSize: 12, opacity: 0.6 }, children: [
3554
+ s.scopeType,
3555
+ " \xB7 ",
3556
+ s.roleName
3557
+ ] })
3558
+ ]
3559
+ },
3560
+ s.membershipId
3561
+ )) })
3562
+ ) : mfa ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("mfa.title"), children: [
3366
3563
  !mfa.backup && mfa.methods.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: t2("mfa.title"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", { style: inputStyle(), value: mfa.selected, onChange: (e) => setMfa({ ...mfa, selected: e.target.value }), children: mfa.methods.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: m, children: m.toUpperCase() }, m)) }) }) : null,
3367
3564
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: mfa.backup ? t2("mfa.backupCodeLabel") : t2("mfa.totpLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3368
3565
  "input",
@@ -3654,9 +3851,12 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
3654
3851
  const t2 = useT();
3655
3852
  const branding = useResolvedSdkBranding(iqAuthBaseUrl);
3656
3853
  const accent = branding?.accentColor || "#6366f1";
3854
+ const { manager } = useCtx();
3657
3855
  const [memberships, setMemberships] = (0, import_react.useState)([]);
3658
3856
  const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
3659
3857
  const [open, setOpen] = (0, import_react.useState)(false);
3858
+ const [pendingStep, setPendingStep] = (0, import_react.useState)(null);
3859
+ const [switchError, setSwitchError] = (0, import_react.useState)(null);
3660
3860
  (0, import_react.useEffect)(() => {
3661
3861
  fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
3662
3862
  if (p?.data?.tenantId) setActiveTenantId(p.data.tenantId);
@@ -3666,18 +3866,48 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
3666
3866
  });
3667
3867
  }, [iqAuthBaseUrl]);
3668
3868
  const switchTo = async (tenantId) => {
3869
+ setSwitchError(null);
3870
+ setPendingStep(null);
3669
3871
  try {
3670
- await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/select-tenant`, {
3671
- method: "POST",
3672
- headers: { "Content-Type": "application/json" },
3673
- body: JSON.stringify({ tenantId })
3674
- });
3872
+ const result = await performTenantSwitch(manager, iqAuthBaseUrl, tenantId);
3873
+ if (result.kind === "mfa_required") {
3874
+ setPendingStep({
3875
+ kind: "mfa_required",
3876
+ tenantId: result.tenantId,
3877
+ mfaChallengeToken: result.mfaChallengeToken,
3878
+ availableMethods: result.availableMethods
3879
+ });
3880
+ return;
3881
+ }
3882
+ if (result.kind === "scope_selection_required") {
3883
+ setPendingStep({
3884
+ kind: "scope_selection_required",
3885
+ tenantId: result.tenantId,
3886
+ scopeSelectionToken: result.scopeSelectionToken
3887
+ });
3888
+ return;
3889
+ }
3675
3890
  setActiveTenantId(tenantId);
3676
3891
  setOpen(false);
3677
3892
  onSwitched?.(tenantId);
3678
- } catch {
3893
+ } catch (err) {
3894
+ setSwitchError(err instanceof Error ? err.message : "Failed to switch organization");
3679
3895
  }
3680
3896
  };
3897
+ const hostedSignInUrl = () => {
3898
+ const baseHosted = `${iqAuthBaseUrl.replace(/\/$/, "")}/sign-in`;
3899
+ if (!pendingStep) return baseHosted;
3900
+ const params = new URLSearchParams();
3901
+ if (pendingStep.kind === "mfa_required") {
3902
+ params.set("mfaChallengeToken", pendingStep.mfaChallengeToken);
3903
+ if (pendingStep.availableMethods.length) {
3904
+ params.set("availableMethods", pendingStep.availableMethods.join(","));
3905
+ }
3906
+ } else {
3907
+ params.set("prompt", "login");
3908
+ }
3909
+ return `${baseHosted}?${params.toString()}`;
3910
+ };
3681
3911
  const active = memberships.find((m) => m.tenantId === activeTenantId);
3682
3912
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3683
3913
  "div",
@@ -3698,44 +3928,274 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
3698
3928
  children: active?.tenantName || active?.tenantSlug || t2("orgSwitcher.label")
3699
3929
  }
3700
3930
  ),
3701
- open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", style: {
3931
+ open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "menu", style: {
3702
3932
  position: "absolute",
3703
3933
  left: 0,
3704
3934
  top: 36,
3705
- minWidth: 220,
3935
+ minWidth: 260,
3706
3936
  background: "#fff",
3707
3937
  border: "1px solid rgba(15,23,42,0.12)",
3708
3938
  borderRadius: 8,
3709
3939
  boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
3710
3940
  padding: 8,
3711
3941
  zIndex: 100
3712
- }, children: memberships.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6, padding: "4px 6px" }, children: t2("orgSwitcher.noOrgs") }) : memberships.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3942
+ }, children: [
3943
+ memberships.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6, padding: "4px 6px" }, children: t2("orgSwitcher.noOrgs") }) : memberships.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3944
+ "button",
3945
+ {
3946
+ role: "menuitem",
3947
+ type: "button",
3948
+ onClick: () => switchTo(m.tenantId),
3949
+ style: {
3950
+ display: "block",
3951
+ width: "100%",
3952
+ textAlign: "left",
3953
+ padding: "8px 10px",
3954
+ background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
3955
+ border: "none",
3956
+ borderRadius: 4,
3957
+ cursor: "pointer",
3958
+ fontSize: 13,
3959
+ color: "#0f172a"
3960
+ },
3961
+ children: [
3962
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
3963
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
3964
+ ]
3965
+ },
3966
+ m.tenantId
3967
+ )),
3968
+ pendingStep ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3969
+ "div",
3970
+ {
3971
+ "data-testid": pendingStep.kind === "mfa_required" ? "prompt-org-switch-mfa-required" : "prompt-org-switch-scope-required",
3972
+ role: "alert",
3973
+ style: {
3974
+ marginTop: 8,
3975
+ padding: "10px 12px",
3976
+ background: `${accent}10`,
3977
+ border: `1px solid ${accent}55`,
3978
+ borderRadius: 6
3979
+ },
3980
+ children: [
3981
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, fontWeight: 600, color: "#0f172a", marginBottom: 4 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredTitle") : t2("orgSwitcher.scopeSelectionRequiredTitle") }),
3982
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: "#334155", marginBottom: 8 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredBody") : t2("orgSwitcher.scopeSelectionRequiredBody") }),
3983
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3984
+ "a",
3985
+ {
3986
+ href: hostedSignInUrl(),
3987
+ "data-testid": "link-org-switch-continue-hosted",
3988
+ style: {
3989
+ display: "inline-block",
3990
+ fontSize: 12,
3991
+ fontWeight: 600,
3992
+ color: branding?.accentColor || accent,
3993
+ textDecoration: "none"
3994
+ },
3995
+ children: t2("orgSwitcher.continueInHostedSignIn")
3996
+ }
3997
+ )
3998
+ ]
3999
+ }
4000
+ ) : null,
4001
+ switchError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-org-switch-error", style: { fontSize: 12, color: "#b91c1c", padding: "6px 8px 0" }, children: switchError }) : null
4002
+ ] }) : null
4003
+ ]
4004
+ }
4005
+ );
4006
+ }
4007
+ function useMemberships() {
4008
+ const { manager, snapshot } = useCtx();
4009
+ const [memberships, setMemberships] = (0, import_react.useState)([]);
4010
+ const [isLoading, setIsLoading] = (0, import_react.useState)(true);
4011
+ const [error, setError] = (0, import_react.useState)(null);
4012
+ const base = manager.issuerUrl.replace(/\/$/, "");
4013
+ const refresh = (0, import_react.useCallback)(async () => {
4014
+ setIsLoading(true);
4015
+ setError(null);
4016
+ try {
4017
+ const res = await manager.fetch(`${base}/api/v1/auth/available-scopes`);
4018
+ const json = await res.json().catch(() => ({}));
4019
+ if (!res.ok) throw new Error(json?.error?.message || `HTTP ${res.status}`);
4020
+ const tree = json.data || {};
4021
+ const flat = [];
4022
+ for (const v of tree.vendors || []) {
4023
+ flat.push({
4024
+ membershipId: v.membershipId,
4025
+ scopeType: "vendor",
4026
+ scopeId: v.id,
4027
+ scopeName: v.name,
4028
+ role: v.role,
4029
+ grantedVia: v.grantedVia || "direct"
4030
+ });
4031
+ }
4032
+ for (const s of tree.sources || []) {
4033
+ flat.push({
4034
+ membershipId: s.membershipId,
4035
+ scopeType: "source",
4036
+ scopeId: s.id,
4037
+ scopeName: s.name,
4038
+ role: s.role,
4039
+ grantedVia: s.grantedVia || "direct"
4040
+ });
4041
+ }
4042
+ for (const c of tree.clients || []) {
4043
+ flat.push({
4044
+ membershipId: c.membershipId,
4045
+ scopeType: "client",
4046
+ scopeId: c.id,
4047
+ scopeName: c.name,
4048
+ role: c.role,
4049
+ grantedVia: c.grantedVia || "direct"
4050
+ });
4051
+ }
4052
+ setMemberships(flat);
4053
+ } catch (err) {
4054
+ setError(err.message);
4055
+ setMemberships([]);
4056
+ } finally {
4057
+ setIsLoading(false);
4058
+ }
4059
+ }, [manager, base]);
4060
+ (0, import_react.useEffect)(() => {
4061
+ if (snapshot.status !== "authenticated") {
4062
+ setMemberships([]);
4063
+ setIsLoading(false);
4064
+ return;
4065
+ }
4066
+ void refresh();
4067
+ }, [snapshot.status, snapshot.tenantId, refresh]);
4068
+ const switchScope = (0, import_react.useCallback)(
4069
+ (target) => performScopeSwitch(manager, base, target),
4070
+ [manager, base]
4071
+ );
4072
+ const active = (0, import_react.useMemo)(() => {
4073
+ const ctx = snapshot.user?.scopeContext;
4074
+ if (!ctx) return null;
4075
+ return {
4076
+ type: ctx.type,
4077
+ id: ctx.id,
4078
+ role: ctx.role,
4079
+ membershipId: ctx.membershipId
4080
+ };
4081
+ }, [snapshot.user, snapshot.version]);
4082
+ return { isLoading, error, memberships, active, refresh, switchScope };
4083
+ }
4084
+ function ScopeSwitcher({ onSwitched, include, className }) {
4085
+ const { memberships, active, isLoading, error, switchScope } = useMemberships();
4086
+ const [open, setOpen] = (0, import_react.useState)(false);
4087
+ const [busy, setBusy] = (0, import_react.useState)(null);
4088
+ const [switchError, setSwitchError] = (0, import_react.useState)(null);
4089
+ const visible = (0, import_react.useMemo)(
4090
+ () => include ? memberships.filter((m) => include.includes(m.scopeType)) : memberships,
4091
+ [memberships, include]
4092
+ );
4093
+ if (isLoading) {
4094
+ return (0, import_react.createElement)(
4095
+ "div",
4096
+ { className, "data-testid": "scope-switcher-loading", style: { fontSize: 13, opacity: 0.6 } },
4097
+ "Loading scopes\u2026"
4098
+ );
4099
+ }
4100
+ if (error) {
4101
+ return (0, import_react.createElement)(
4102
+ "div",
4103
+ { className, "data-testid": "scope-switcher-error", style: { fontSize: 13, color: "#b91c1c" } },
4104
+ error
4105
+ );
4106
+ }
4107
+ if (!visible.length) return null;
4108
+ const activeLabel = active ? visible.find((m) => m.scopeType === active.type && m.scopeId === active.id)?.scopeName || `${active.type}:${active.id}` : "Select a scope";
4109
+ return (0, import_react.createElement)(
4110
+ "div",
4111
+ { className, "data-testid": "scope-switcher", style: { position: "relative", display: "inline-block" } },
4112
+ (0, import_react.createElement)(
4113
+ "button",
4114
+ {
4115
+ type: "button",
4116
+ "data-testid": "scope-switcher-trigger",
4117
+ onClick: () => setOpen((v) => !v),
4118
+ style: {
4119
+ padding: "6px 10px",
4120
+ border: "1px solid #e5e7eb",
4121
+ borderRadius: 6,
4122
+ background: "#fff",
4123
+ cursor: "pointer",
4124
+ fontSize: 13
4125
+ }
4126
+ },
4127
+ active ? `${active.type}: ${activeLabel}` : activeLabel
4128
+ ),
4129
+ switchError ? (0, import_react.createElement)(
4130
+ "div",
4131
+ {
4132
+ "data-testid": "scope-switcher-switch-error",
4133
+ style: { marginTop: 4, fontSize: 12, color: "#b91c1c" }
4134
+ },
4135
+ switchError
4136
+ ) : null,
4137
+ open ? (0, import_react.createElement)(
4138
+ "div",
4139
+ {
4140
+ "data-testid": "scope-switcher-menu",
4141
+ style: {
4142
+ position: "absolute",
4143
+ top: "100%",
4144
+ left: 0,
4145
+ marginTop: 4,
4146
+ minWidth: 220,
4147
+ background: "#fff",
4148
+ border: "1px solid #e5e7eb",
4149
+ borderRadius: 6,
4150
+ boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
4151
+ padding: 4,
4152
+ zIndex: 50
4153
+ }
4154
+ },
4155
+ visible.map((m) => {
4156
+ const isActive = active?.type === m.scopeType && active?.id === m.scopeId;
4157
+ const key = `${m.scopeType}:${m.scopeId}`;
4158
+ return (0, import_react.createElement)(
3713
4159
  "button",
3714
4160
  {
3715
- role: "menuitem",
4161
+ key,
3716
4162
  type: "button",
3717
- onClick: () => switchTo(m.tenantId),
4163
+ "data-testid": `scope-switcher-option-${m.scopeType}-${m.scopeId}`,
4164
+ disabled: busy === key,
4165
+ onClick: async () => {
4166
+ setBusy(key);
4167
+ setSwitchError(null);
4168
+ try {
4169
+ await switchScope({ type: m.scopeType, id: m.scopeId });
4170
+ setOpen(false);
4171
+ onSwitched?.({ type: m.scopeType, id: m.scopeId });
4172
+ } catch (err) {
4173
+ setSwitchError(err.message || "Scope switch failed");
4174
+ } finally {
4175
+ setBusy(null);
4176
+ }
4177
+ },
3718
4178
  style: {
3719
4179
  display: "block",
3720
4180
  width: "100%",
3721
4181
  textAlign: "left",
3722
4182
  padding: "8px 10px",
3723
- background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
3724
4183
  border: "none",
4184
+ background: isActive ? "#f3f4f6" : "transparent",
4185
+ cursor: busy ? "wait" : "pointer",
3725
4186
  borderRadius: 4,
3726
- cursor: "pointer",
3727
- fontSize: 13,
3728
- color: "#0f172a"
3729
- },
3730
- children: [
3731
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
3732
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
3733
- ]
4187
+ fontSize: 13
4188
+ }
3734
4189
  },
3735
- m.tenantId
3736
- )) }) : null
3737
- ]
3738
- }
4190
+ (0, import_react.createElement)("div", { style: { fontWeight: 500 } }, m.scopeName),
4191
+ (0, import_react.createElement)(
4192
+ "div",
4193
+ { style: { fontSize: 11, opacity: 0.6 } },
4194
+ `${m.scopeType} \xB7 ${m.role}${m.grantedVia && m.grantedVia !== "direct" ? ` \xB7 via ${m.grantedVia}` : ""}`
4195
+ )
4196
+ );
4197
+ })
4198
+ ) : null
3739
4199
  );
3740
4200
  }
3741
4201
  function useImpersonation() {
@@ -4348,10 +4808,13 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
4348
4808
  const branding = useResolvedSdkBranding(iqAuthBaseUrl);
4349
4809
  const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
4350
4810
  const accent = branding?.accentColor || "#6366f1";
4811
+ const t2 = useT();
4812
+ const { manager } = useCtx();
4351
4813
  const [memberships, setMemberships] = (0, import_react.useState)([]);
4352
4814
  const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
4353
4815
  const [loading, setLoading] = (0, import_react.useState)(true);
4354
4816
  const [error, setError] = (0, import_react.useState)(null);
4817
+ const [pendingStep, setPendingStep] = (0, import_react.useState)(null);
4355
4818
  (0, import_react.useEffect)(() => {
4356
4819
  let cancelled = false;
4357
4820
  Promise.all([
@@ -4375,21 +4838,81 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
4375
4838
  onSelect?.(tenantId);
4376
4839
  return;
4377
4840
  }
4841
+ setError(null);
4842
+ setPendingStep(null);
4378
4843
  try {
4379
- await jsonFetch(`${baseUrl}/api/v1/auth/select-tenant`, {
4380
- method: "POST",
4381
- headers: { "Content-Type": "application/json" },
4382
- credentials: "include",
4383
- body: JSON.stringify({ tenantId })
4384
- });
4844
+ const result = await performTenantSwitch(manager, iqAuthBaseUrl, tenantId);
4845
+ if (result.kind === "mfa_required") {
4846
+ setPendingStep({
4847
+ kind: "mfa_required",
4848
+ tenantId: result.tenantId,
4849
+ mfaChallengeToken: result.mfaChallengeToken,
4850
+ availableMethods: result.availableMethods
4851
+ });
4852
+ return;
4853
+ }
4854
+ if (result.kind === "scope_selection_required") {
4855
+ setPendingStep({
4856
+ kind: "scope_selection_required",
4857
+ tenantId: result.tenantId,
4858
+ scopeSelectionToken: result.scopeSelectionToken
4859
+ });
4860
+ return;
4861
+ }
4385
4862
  setActiveTenantId(tenantId);
4386
4863
  onSelect?.(tenantId);
4387
4864
  } catch (err) {
4388
4865
  setError(err instanceof Error ? err.message : "Failed to switch organization");
4389
4866
  }
4390
4867
  };
4868
+ const hostedSignInUrl = () => {
4869
+ const baseHosted = `${baseUrl}/sign-in`;
4870
+ if (!pendingStep) return baseHosted;
4871
+ const params = new URLSearchParams();
4872
+ if (pendingStep.kind === "mfa_required") {
4873
+ params.set("mfaChallengeToken", pendingStep.mfaChallengeToken);
4874
+ if (pendingStep.availableMethods.length) {
4875
+ params.set("availableMethods", pendingStep.availableMethods.join(","));
4876
+ }
4877
+ } else {
4878
+ params.set("prompt", "login");
4879
+ }
4880
+ return `${baseHosted}?${params.toString()}`;
4881
+ };
4391
4882
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding, className, title: "Your organizations", subtitle: "Select an organization to make it active.", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-iqauth-sdk-org-list": "", style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
4392
4883
  error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
4884
+ pendingStep ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
4885
+ "div",
4886
+ {
4887
+ "data-testid": pendingStep.kind === "mfa_required" ? "prompt-org-list-mfa-required" : "prompt-org-list-scope-required",
4888
+ role: "alert",
4889
+ style: {
4890
+ padding: "10px 12px",
4891
+ background: `${accent}10`,
4892
+ border: `1px solid ${accent}55`,
4893
+ borderRadius: 6
4894
+ },
4895
+ children: [
4896
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a", marginBottom: 4 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredTitle") : t2("orgSwitcher.scopeSelectionRequiredTitle") }),
4897
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: "#334155", marginBottom: 8 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredBody") : t2("orgSwitcher.scopeSelectionRequiredBody") }),
4898
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
4899
+ "a",
4900
+ {
4901
+ href: hostedSignInUrl(),
4902
+ "data-testid": "link-org-list-continue-hosted",
4903
+ style: {
4904
+ display: "inline-block",
4905
+ fontSize: 12,
4906
+ fontWeight: 600,
4907
+ color: branding?.accentColor || accent,
4908
+ textDecoration: "none"
4909
+ },
4910
+ children: t2("orgSwitcher.continueInHostedSignIn")
4911
+ }
4912
+ )
4913
+ ]
4914
+ }
4915
+ ) : null,
4393
4916
  loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13 }, children: "Loading\u2026" }) : memberships.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6 }, children: "You don\u2019t belong to any organizations yet." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: memberships.map((m) => {
4394
4917
  const active = m.tenantId === activeTenantId;
4395
4918
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
@@ -4427,8 +4950,8 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
4427
4950
  iqAuthBaseUrl,
4428
4951
  unstyled: true,
4429
4952
  appearance,
4430
- onCreated: (t2) => {
4431
- onSelect?.(t2.id);
4953
+ onCreated: (t3) => {
4954
+ onSelect?.(t3.id);
4432
4955
  reloadList();
4433
4956
  },
4434
4957
  redirectUrl: createRedirectUrl
@@ -4681,6 +5204,7 @@ var __version__ = "phase-bc-1.0.0";
4681
5204
  Protect,
4682
5205
  RedirectToSignIn,
4683
5206
  RedirectToSignedIn,
5207
+ ScopeSwitcher,
4684
5208
  SignIn,
4685
5209
  SignUp,
4686
5210
  SignedIn,
@@ -4690,9 +5214,13 @@ var __version__ = "phase-bc-1.0.0";
4690
5214
  Waitlist,
4691
5215
  __useIQAuthInternal,
4692
5216
  __version__,
5217
+ claimSatisfiesScope,
4693
5218
  isReturnToAllowed,
4694
5219
  isSilentSsoEligible,
5220
+ performScopeSwitch,
5221
+ performTenantSwitch,
4695
5222
  preflightReturnTo,
5223
+ resolveAfterSignInDestination,
4696
5224
  revokeSession,
4697
5225
  sanitizeBrandCss,
4698
5226
  sanitizeReturnTo,
@@ -4706,6 +5234,7 @@ var __version__ = "phase-bc-1.0.0";
4706
5234
  useLinkedIdentities,
4707
5235
  useLocale,
4708
5236
  useMagicLink,
5237
+ useMemberships,
4709
5238
  useOrganization,
4710
5239
  usePasskey,
4711
5240
  useResolvedSdkBranding,