@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
@@ -9,60 +9,22 @@ import {
9
9
  requestMagicLink,
10
10
  signInWithPasskey,
11
11
  unlinkProvider
12
- } from "./chunk-RTJAIBXY.mjs";
12
+ } from "./chunk-4V7FKOTG.mjs";
13
13
  import {
14
14
  handleAuthCallback,
15
15
  redirectToSignIn,
16
16
  signIn,
17
17
  signOut
18
18
  } from "./chunk-GN37E64I.mjs";
19
+ import {
20
+ sanitizeReturnTo
21
+ } from "./chunk-JRDVUWAL.mjs";
19
22
  import {
20
23
  defaultBundle,
21
24
  localizeErrorCode,
22
25
  resolveBundle,
23
26
  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
- }
27
+ } from "./chunk-TLET552H.mjs";
66
28
 
67
29
  // src/react/index.tsx
68
30
  import {
@@ -495,6 +457,65 @@ function RedirectToSignIn(props = {}) {
495
457
  }
496
458
  return null;
497
459
  }
460
+ async function performScopeSwitch(manager, base, target) {
461
+ const res = await manager.fetch(`${base}/api/v1/auth/switch-scope`, {
462
+ method: "POST",
463
+ headers: { "Content-Type": "application/json" },
464
+ body: JSON.stringify({ scopeType: target.type, scopeId: target.id })
465
+ });
466
+ const body = await res.json().catch(() => ({}));
467
+ if (!res.ok) {
468
+ throw new Error(body?.error?.message || `HTTP ${res.status}`);
469
+ }
470
+ const newToken = body?.data?.accessToken;
471
+ if (newToken) {
472
+ manager.adoptAccessToken(newToken);
473
+ void manager.refresh().catch(() => void 0);
474
+ return;
475
+ }
476
+ const ok = await manager.refresh();
477
+ if (!ok) {
478
+ throw new Error("scope switch succeeded server-side but token refresh failed; reload to recover");
479
+ }
480
+ }
481
+ async function performTenantSwitch(manager, base, tenantId) {
482
+ const res = await manager.fetch(`${base.replace(/\/$/, "")}/api/v1/auth/select-tenant`, {
483
+ method: "POST",
484
+ headers: { "Content-Type": "application/json" },
485
+ body: JSON.stringify({ tenantId })
486
+ });
487
+ const body = await res.json().catch(() => ({}));
488
+ if (!res.ok) {
489
+ throw new Error(body?.error?.message || `HTTP ${res.status}`);
490
+ }
491
+ if (body?.data?.mfaChallengeToken) {
492
+ return {
493
+ kind: "mfa_required",
494
+ tenantId: body?.data?.tenantId || tenantId,
495
+ mfaChallengeToken: body.data.mfaChallengeToken,
496
+ availableMethods: Array.isArray(body.data.availableMethods) ? body.data.availableMethods : []
497
+ };
498
+ }
499
+ if (body?.data?.type === "scope_selection" || body?.data?.scopeSelectionToken) {
500
+ return {
501
+ kind: "scope_selection_required",
502
+ tenantId: body?.data?.tenantId || tenantId,
503
+ scopeSelectionToken: body?.data?.scopeSelectionToken || "",
504
+ scopes: Array.isArray(body?.data?.scopes) ? body.data.scopes : []
505
+ };
506
+ }
507
+ const newToken = body?.data?.accessToken;
508
+ if (newToken) {
509
+ manager.adoptAccessToken(newToken);
510
+ void manager.refresh().catch(() => void 0);
511
+ return { kind: "ok", tenantId: body?.data?.user?.tenantId || tenantId };
512
+ }
513
+ const ok = await manager.refresh();
514
+ if (!ok) {
515
+ throw new Error("tenant switch succeeded server-side but token refresh failed; reload to recover");
516
+ }
517
+ return { kind: "ok", tenantId };
518
+ }
498
519
  function asArray(v) {
499
520
  if (v == null) return [];
500
521
  return Array.isArray(v) ? v : [v];
@@ -521,7 +542,14 @@ function claimPermissions(c) {
521
542
  }
522
543
  return Array.from(out);
523
544
  }
524
- function Protect({ role, permission, condition, fallback = null, children }) {
545
+ function claimSatisfiesScope(claims, required) {
546
+ if (!claims) return false;
547
+ const ctx = claims.scopeContext;
548
+ if (!ctx || !ctx.type || !ctx.id) return false;
549
+ const list = Array.isArray(required) ? required : [required];
550
+ return list.some((r) => r.type === ctx.type && r.id === ctx.id);
551
+ }
552
+ function Protect({ role, permission, scope, condition, fallback = null, children }) {
525
553
  const { snapshot } = useCtx();
526
554
  if (snapshot.status !== "authenticated") return createElement(Fragment, null, fallback);
527
555
  const wantedRoles = asArray(role);
@@ -534,6 +562,9 @@ function Protect({ role, permission, condition, fallback = null, children }) {
534
562
  const have = new Set(claimPermissions(snapshot.claims));
535
563
  if (!wantedPerms.some((p) => have.has(p))) return createElement(Fragment, null, fallback);
536
564
  }
565
+ if (scope) {
566
+ if (!claimSatisfiesScope(snapshot.claims, scope)) return createElement(Fragment, null, fallback);
567
+ }
537
568
  if (condition && !condition(snapshot.claims)) return createElement(Fragment, null, fallback);
538
569
  return createElement(Fragment, null, children);
539
570
  }
@@ -1090,12 +1121,54 @@ function isSilentSsoEligible(ctx, effectivePrompt) {
1090
1121
  if (!ctx.returnAllowed) return false;
1091
1122
  return true;
1092
1123
  }
1124
+ function resolveAfterSignInDestination(args) {
1125
+ let raw = args.prop ?? null;
1126
+ if (!raw && typeof args.search === "string") {
1127
+ try {
1128
+ const params = new URLSearchParams(args.search);
1129
+ raw = params.get("return_to") ?? params.get("next");
1130
+ } catch {
1131
+ }
1132
+ }
1133
+ return sanitizeReturnTo(raw, {
1134
+ allowedOrigins: args.allowedOrigins,
1135
+ currentOrigin: args.currentOrigin,
1136
+ fallback: "/"
1137
+ });
1138
+ }
1093
1139
  function SignIn(props) {
1094
1140
  const providerCtx = useContext(IQAuthContext);
1095
1141
  const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.hostedIssuerUrl ?? "";
1096
1142
  const appKey = props.appKey ?? providerCtx?.manager.appKey ?? "";
1097
1143
  const returnTo = props.returnTo ?? (typeof window !== "undefined" ? `${window.location.origin}/api/iqauth/callback` : "");
1098
- const { onRedirect, className, prompt, appearance: instanceAppearance, silentSso: instanceSilentSso } = props;
1144
+ const afterSignInUrl = resolveAfterSignInDestination({
1145
+ prop: props.afterSignInUrl,
1146
+ search: typeof window !== "undefined" ? window.location.search : "",
1147
+ allowedOrigins: providerCtx?.allowedReturnOrigins
1148
+ });
1149
+ const { onRedirect, className, prompt, appearance: instanceAppearance, silentSso: instanceSilentSso, scopeHint: scopeHintProp } = props;
1150
+ const effectiveScopeHint = (() => {
1151
+ const fromProp = scopeHintProp ?? null;
1152
+ let raw = fromProp;
1153
+ if (!raw && typeof window !== "undefined") {
1154
+ try {
1155
+ raw = new URLSearchParams(window.location.search).get("scope_hint");
1156
+ } catch {
1157
+ }
1158
+ }
1159
+ if (!raw) return null;
1160
+ if (typeof raw === "object" && raw && raw.type && raw.id) {
1161
+ const t3 = raw.type;
1162
+ if (t3 === "vendor" || t3 === "source" || t3 === "client") return { type: t3, id: String(raw.id) };
1163
+ return null;
1164
+ }
1165
+ if (typeof raw === "string" && raw.includes(":")) {
1166
+ const [t3, id] = raw.split(":", 2);
1167
+ if ((t3 === "vendor" || t3 === "source" || t3 === "client") && id) return { type: t3, id };
1168
+ }
1169
+ return null;
1170
+ })();
1171
+ const scopeHintBody = effectiveScopeHint ? { scopeHint: effectiveScopeHint } : {};
1099
1172
  const silentSsoEnabled = instanceSilentSso ?? providerCtx?.silentSso ?? false;
1100
1173
  const appearance = instanceAppearance && providerCtx?.appearance ? { elements: { ...providerCtx.appearance.elements, ...instanceAppearance.elements } } : instanceAppearance ?? providerCtx?.appearance ?? null;
1101
1174
  if (!iqAuthBaseUrl || !appKey) {
@@ -1116,6 +1189,11 @@ function SignIn(props) {
1116
1189
  appKey
1117
1190
  });
1118
1191
  if (guardError) throw guardError;
1192
+ useEffect(() => {
1193
+ if (typeof document === "undefined") return;
1194
+ const attrs = "; path=/; SameSite=Lax" + (typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "");
1195
+ document.cookie = `iqauth_return_to=${encodeURIComponent(afterSignInUrl)}${attrs}`;
1196
+ }, [afterSignInUrl]);
1119
1197
  const preflightLoggedRef = useRef(false);
1120
1198
  useEffect(() => {
1121
1199
  if (!ctx || preflightLoggedRef.current) return;
@@ -1131,6 +1209,7 @@ function SignIn(props) {
1131
1209
  const [formError, setFormError] = useState("");
1132
1210
  const [mfa, setMfa] = useState(null);
1133
1211
  const [tenantSel, setTenantSel] = useState(null);
1212
+ const [scopeSel, setScopeSel] = useState(null);
1134
1213
  const [oauthExchanging, setOauthExchanging] = useState(false);
1135
1214
  const [silent, setSilent] = useState("idle");
1136
1215
  const [forcePrompt, setForcePrompt] = useState(false);
@@ -1161,6 +1240,12 @@ function SignIn(props) {
1161
1240
  setTenantSel({ token: payload.tenantSelectionToken, tenants: payload.tenants || [] });
1162
1241
  return true;
1163
1242
  }
1243
+ if (payload.type === "scope_selection") {
1244
+ setScopeSel({ token: payload.scopeSelectionToken, tenantId: payload.tenantId, scopes: payload.scopes || [] });
1245
+ setTenantSel(null);
1246
+ setMfa(null);
1247
+ return true;
1248
+ }
1164
1249
  if (payload.type === "mfa_required") {
1165
1250
  const methods = payload.availableMethods || ["totp"];
1166
1251
  setMfa({ token: payload.mfaChallengeToken, methods, selected: methods[0], code: "", backup: false });
@@ -1181,7 +1266,7 @@ function SignIn(props) {
1181
1266
  method: "POST",
1182
1267
  headers: { "Content-Type": "application/json" },
1183
1268
  credentials: "include",
1184
- body: JSON.stringify({ email, password, ...oidcPayload() })
1269
+ body: JSON.stringify({ email, password, ...oidcPayload(), ...scopeHintBody })
1185
1270
  });
1186
1271
  const payload = await r.json().catch(() => ({}));
1187
1272
  if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
@@ -1204,7 +1289,8 @@ function SignIn(props) {
1204
1289
  code: mfa.code,
1205
1290
  method: mfa.selected,
1206
1291
  useBackup: mfa.backup,
1207
- ...oidcPayload()
1292
+ ...oidcPayload(),
1293
+ ...scopeHintBody
1208
1294
  })
1209
1295
  });
1210
1296
  const payload = await r.json().catch(() => ({}));
@@ -1219,7 +1305,21 @@ function SignIn(props) {
1219
1305
  method: "POST",
1220
1306
  headers: { "Content-Type": "application/json" },
1221
1307
  credentials: "include",
1222
- body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload() })
1308
+ body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload(), ...scopeHintBody })
1309
+ });
1310
+ const payload = await r.json().catch(() => ({}));
1311
+ if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
1312
+ setSubmitting(false);
1313
+ };
1314
+ const submitScope = async (membershipId) => {
1315
+ if (!scopeSel) return;
1316
+ setSubmitting(true);
1317
+ setFormError("");
1318
+ const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-scope-select`, {
1319
+ method: "POST",
1320
+ headers: { "Content-Type": "application/json" },
1321
+ credentials: "include",
1322
+ body: JSON.stringify({ scopeSelectionToken: scopeSel.token, membershipId, ...oidcPayload(), ...scopeHintBody })
1223
1323
  });
1224
1324
  const payload = await r.json().catch(() => ({}));
1225
1325
  if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
@@ -1287,6 +1387,11 @@ function SignIn(props) {
1287
1387
  setSilent("failed");
1288
1388
  return;
1289
1389
  }
1390
+ if (payload?.type === "scope_selection") {
1391
+ setScopeSel({ token: payload.scopeSelectionToken, tenantId: payload.tenantId, scopes: payload.scopes || [] });
1392
+ setSilent("failed");
1393
+ return;
1394
+ }
1290
1395
  setSilent("failed");
1291
1396
  } catch {
1292
1397
  setSilent("failed");
@@ -1298,6 +1403,7 @@ function SignIn(props) {
1298
1403
  setForcePrompt(true);
1299
1404
  setSilent("skipped");
1300
1405
  setTenantSel(null);
1406
+ setScopeSel(null);
1301
1407
  if (typeof window !== "undefined") {
1302
1408
  try {
1303
1409
  const u = new URL(window.location.href);
@@ -1368,7 +1474,27 @@ function SignIn(props) {
1368
1474
  ]
1369
1475
  },
1370
1476
  tn.tenantId
1371
- )) }) : mfa ? /* @__PURE__ */ jsxs("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("mfa.title"), children: [
1477
+ )) }) : scopeSel ? (
1478
+ // Task #171 — scope picker (source/client memberships)
1479
+ /* @__PURE__ */ 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__ */ jsxs(
1480
+ "button",
1481
+ {
1482
+ type: "button",
1483
+ "data-iqauth-scope": s.membershipId,
1484
+ onClick: () => submitScope(s.membershipId),
1485
+ style: { textAlign: "left", padding: "10px 14px", border: "1px solid rgba(15,23,42,0.15)", borderRadius: 8, background: "transparent", color: "inherit", cursor: "pointer" },
1486
+ children: [
1487
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontWeight: 500 }, children: s.scopeName }),
1488
+ /* @__PURE__ */ jsxs("p", { style: { margin: 0, fontSize: 12, opacity: 0.6 }, children: [
1489
+ s.scopeType,
1490
+ " \xB7 ",
1491
+ s.roleName
1492
+ ] })
1493
+ ]
1494
+ },
1495
+ s.membershipId
1496
+ )) })
1497
+ ) : mfa ? /* @__PURE__ */ jsxs("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("mfa.title"), children: [
1372
1498
  !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
1499
  /* @__PURE__ */ jsx(Field, { label: mfa.backup ? t2("mfa.backupCodeLabel") : t2("mfa.totpLabel"), children: /* @__PURE__ */ jsx(
1374
1500
  "input",
@@ -1660,9 +1786,12 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
1660
1786
  const t2 = useT();
1661
1787
  const branding = useResolvedSdkBranding(iqAuthBaseUrl);
1662
1788
  const accent = branding?.accentColor || "#6366f1";
1789
+ const { manager } = useCtx();
1663
1790
  const [memberships, setMemberships] = useState([]);
1664
1791
  const [activeTenantId, setActiveTenantId] = useState(null);
1665
1792
  const [open, setOpen] = useState(false);
1793
+ const [pendingStep, setPendingStep] = useState(null);
1794
+ const [switchError, setSwitchError] = useState(null);
1666
1795
  useEffect(() => {
1667
1796
  fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
1668
1797
  if (p?.data?.tenantId) setActiveTenantId(p.data.tenantId);
@@ -1672,17 +1801,47 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
1672
1801
  });
1673
1802
  }, [iqAuthBaseUrl]);
1674
1803
  const switchTo = async (tenantId) => {
1804
+ setSwitchError(null);
1805
+ setPendingStep(null);
1675
1806
  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
- });
1807
+ const result = await performTenantSwitch(manager, iqAuthBaseUrl, tenantId);
1808
+ if (result.kind === "mfa_required") {
1809
+ setPendingStep({
1810
+ kind: "mfa_required",
1811
+ tenantId: result.tenantId,
1812
+ mfaChallengeToken: result.mfaChallengeToken,
1813
+ availableMethods: result.availableMethods
1814
+ });
1815
+ return;
1816
+ }
1817
+ if (result.kind === "scope_selection_required") {
1818
+ setPendingStep({
1819
+ kind: "scope_selection_required",
1820
+ tenantId: result.tenantId,
1821
+ scopeSelectionToken: result.scopeSelectionToken
1822
+ });
1823
+ return;
1824
+ }
1681
1825
  setActiveTenantId(tenantId);
1682
1826
  setOpen(false);
1683
1827
  onSwitched?.(tenantId);
1684
- } catch {
1828
+ } catch (err) {
1829
+ setSwitchError(err instanceof Error ? err.message : "Failed to switch organization");
1830
+ }
1831
+ };
1832
+ const hostedSignInUrl = () => {
1833
+ const baseHosted = `${iqAuthBaseUrl.replace(/\/$/, "")}/sign-in`;
1834
+ if (!pendingStep) return baseHosted;
1835
+ const params = new URLSearchParams();
1836
+ if (pendingStep.kind === "mfa_required") {
1837
+ params.set("mfaChallengeToken", pendingStep.mfaChallengeToken);
1838
+ if (pendingStep.availableMethods.length) {
1839
+ params.set("availableMethods", pendingStep.availableMethods.join(","));
1840
+ }
1841
+ } else {
1842
+ params.set("prompt", "login");
1685
1843
  }
1844
+ return `${baseHosted}?${params.toString()}`;
1686
1845
  };
1687
1846
  const active = memberships.find((m) => m.tenantId === activeTenantId);
1688
1847
  return /* @__PURE__ */ jsxs(
@@ -1704,44 +1863,274 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
1704
1863
  children: active?.tenantName || active?.tenantSlug || t2("orgSwitcher.label")
1705
1864
  }
1706
1865
  ),
1707
- open ? /* @__PURE__ */ jsx("div", { role: "menu", style: {
1866
+ open ? /* @__PURE__ */ jsxs("div", { role: "menu", style: {
1708
1867
  position: "absolute",
1709
1868
  left: 0,
1710
1869
  top: 36,
1711
- minWidth: 220,
1870
+ minWidth: 260,
1712
1871
  background: "#fff",
1713
1872
  border: "1px solid rgba(15,23,42,0.12)",
1714
1873
  borderRadius: 8,
1715
1874
  boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
1716
1875
  padding: 8,
1717
1876
  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(
1877
+ }, children: [
1878
+ memberships.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, opacity: 0.6, padding: "4px 6px" }, children: t2("orgSwitcher.noOrgs") }) : memberships.map((m) => /* @__PURE__ */ jsxs(
1879
+ "button",
1880
+ {
1881
+ role: "menuitem",
1882
+ type: "button",
1883
+ onClick: () => switchTo(m.tenantId),
1884
+ style: {
1885
+ display: "block",
1886
+ width: "100%",
1887
+ textAlign: "left",
1888
+ padding: "8px 10px",
1889
+ background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
1890
+ border: "none",
1891
+ borderRadius: 4,
1892
+ cursor: "pointer",
1893
+ fontSize: 13,
1894
+ color: "#0f172a"
1895
+ },
1896
+ children: [
1897
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
1898
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
1899
+ ]
1900
+ },
1901
+ m.tenantId
1902
+ )),
1903
+ pendingStep ? /* @__PURE__ */ jsxs(
1904
+ "div",
1905
+ {
1906
+ "data-testid": pendingStep.kind === "mfa_required" ? "prompt-org-switch-mfa-required" : "prompt-org-switch-scope-required",
1907
+ role: "alert",
1908
+ style: {
1909
+ marginTop: 8,
1910
+ padding: "10px 12px",
1911
+ background: `${accent}10`,
1912
+ border: `1px solid ${accent}55`,
1913
+ borderRadius: 6
1914
+ },
1915
+ children: [
1916
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12, fontWeight: 600, color: "#0f172a", marginBottom: 4 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredTitle") : t2("orgSwitcher.scopeSelectionRequiredTitle") }),
1917
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#334155", marginBottom: 8 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredBody") : t2("orgSwitcher.scopeSelectionRequiredBody") }),
1918
+ /* @__PURE__ */ jsx(
1919
+ "a",
1920
+ {
1921
+ href: hostedSignInUrl(),
1922
+ "data-testid": "link-org-switch-continue-hosted",
1923
+ style: {
1924
+ display: "inline-block",
1925
+ fontSize: 12,
1926
+ fontWeight: 600,
1927
+ color: branding?.accentColor || accent,
1928
+ textDecoration: "none"
1929
+ },
1930
+ children: t2("orgSwitcher.continueInHostedSignIn")
1931
+ }
1932
+ )
1933
+ ]
1934
+ }
1935
+ ) : null,
1936
+ switchError ? /* @__PURE__ */ jsx("p", { "data-testid": "text-org-switch-error", style: { fontSize: 12, color: "#b91c1c", padding: "6px 8px 0" }, children: switchError }) : null
1937
+ ] }) : null
1938
+ ]
1939
+ }
1940
+ );
1941
+ }
1942
+ function useMemberships() {
1943
+ const { manager, snapshot } = useCtx();
1944
+ const [memberships, setMemberships] = useState([]);
1945
+ const [isLoading, setIsLoading] = useState(true);
1946
+ const [error, setError] = useState(null);
1947
+ const base = manager.issuerUrl.replace(/\/$/, "");
1948
+ const refresh = useCallback(async () => {
1949
+ setIsLoading(true);
1950
+ setError(null);
1951
+ try {
1952
+ const res = await manager.fetch(`${base}/api/v1/auth/available-scopes`);
1953
+ const json = await res.json().catch(() => ({}));
1954
+ if (!res.ok) throw new Error(json?.error?.message || `HTTP ${res.status}`);
1955
+ const tree = json.data || {};
1956
+ const flat = [];
1957
+ for (const v of tree.vendors || []) {
1958
+ flat.push({
1959
+ membershipId: v.membershipId,
1960
+ scopeType: "vendor",
1961
+ scopeId: v.id,
1962
+ scopeName: v.name,
1963
+ role: v.role,
1964
+ grantedVia: v.grantedVia || "direct"
1965
+ });
1966
+ }
1967
+ for (const s of tree.sources || []) {
1968
+ flat.push({
1969
+ membershipId: s.membershipId,
1970
+ scopeType: "source",
1971
+ scopeId: s.id,
1972
+ scopeName: s.name,
1973
+ role: s.role,
1974
+ grantedVia: s.grantedVia || "direct"
1975
+ });
1976
+ }
1977
+ for (const c of tree.clients || []) {
1978
+ flat.push({
1979
+ membershipId: c.membershipId,
1980
+ scopeType: "client",
1981
+ scopeId: c.id,
1982
+ scopeName: c.name,
1983
+ role: c.role,
1984
+ grantedVia: c.grantedVia || "direct"
1985
+ });
1986
+ }
1987
+ setMemberships(flat);
1988
+ } catch (err) {
1989
+ setError(err.message);
1990
+ setMemberships([]);
1991
+ } finally {
1992
+ setIsLoading(false);
1993
+ }
1994
+ }, [manager, base]);
1995
+ useEffect(() => {
1996
+ if (snapshot.status !== "authenticated") {
1997
+ setMemberships([]);
1998
+ setIsLoading(false);
1999
+ return;
2000
+ }
2001
+ void refresh();
2002
+ }, [snapshot.status, snapshot.tenantId, refresh]);
2003
+ const switchScope = useCallback(
2004
+ (target) => performScopeSwitch(manager, base, target),
2005
+ [manager, base]
2006
+ );
2007
+ const active = useMemo(() => {
2008
+ const ctx = snapshot.user?.scopeContext;
2009
+ if (!ctx) return null;
2010
+ return {
2011
+ type: ctx.type,
2012
+ id: ctx.id,
2013
+ role: ctx.role,
2014
+ membershipId: ctx.membershipId
2015
+ };
2016
+ }, [snapshot.user, snapshot.version]);
2017
+ return { isLoading, error, memberships, active, refresh, switchScope };
2018
+ }
2019
+ function ScopeSwitcher({ onSwitched, include, className }) {
2020
+ const { memberships, active, isLoading, error, switchScope } = useMemberships();
2021
+ const [open, setOpen] = useState(false);
2022
+ const [busy, setBusy] = useState(null);
2023
+ const [switchError, setSwitchError] = useState(null);
2024
+ const visible = useMemo(
2025
+ () => include ? memberships.filter((m) => include.includes(m.scopeType)) : memberships,
2026
+ [memberships, include]
2027
+ );
2028
+ if (isLoading) {
2029
+ return createElement(
2030
+ "div",
2031
+ { className, "data-testid": "scope-switcher-loading", style: { fontSize: 13, opacity: 0.6 } },
2032
+ "Loading scopes\u2026"
2033
+ );
2034
+ }
2035
+ if (error) {
2036
+ return createElement(
2037
+ "div",
2038
+ { className, "data-testid": "scope-switcher-error", style: { fontSize: 13, color: "#b91c1c" } },
2039
+ error
2040
+ );
2041
+ }
2042
+ if (!visible.length) return null;
2043
+ const activeLabel = active ? visible.find((m) => m.scopeType === active.type && m.scopeId === active.id)?.scopeName || `${active.type}:${active.id}` : "Select a scope";
2044
+ return createElement(
2045
+ "div",
2046
+ { className, "data-testid": "scope-switcher", style: { position: "relative", display: "inline-block" } },
2047
+ createElement(
2048
+ "button",
2049
+ {
2050
+ type: "button",
2051
+ "data-testid": "scope-switcher-trigger",
2052
+ onClick: () => setOpen((v) => !v),
2053
+ style: {
2054
+ padding: "6px 10px",
2055
+ border: "1px solid #e5e7eb",
2056
+ borderRadius: 6,
2057
+ background: "#fff",
2058
+ cursor: "pointer",
2059
+ fontSize: 13
2060
+ }
2061
+ },
2062
+ active ? `${active.type}: ${activeLabel}` : activeLabel
2063
+ ),
2064
+ switchError ? createElement(
2065
+ "div",
2066
+ {
2067
+ "data-testid": "scope-switcher-switch-error",
2068
+ style: { marginTop: 4, fontSize: 12, color: "#b91c1c" }
2069
+ },
2070
+ switchError
2071
+ ) : null,
2072
+ open ? createElement(
2073
+ "div",
2074
+ {
2075
+ "data-testid": "scope-switcher-menu",
2076
+ style: {
2077
+ position: "absolute",
2078
+ top: "100%",
2079
+ left: 0,
2080
+ marginTop: 4,
2081
+ minWidth: 220,
2082
+ background: "#fff",
2083
+ border: "1px solid #e5e7eb",
2084
+ borderRadius: 6,
2085
+ boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
2086
+ padding: 4,
2087
+ zIndex: 50
2088
+ }
2089
+ },
2090
+ visible.map((m) => {
2091
+ const isActive = active?.type === m.scopeType && active?.id === m.scopeId;
2092
+ const key = `${m.scopeType}:${m.scopeId}`;
2093
+ return createElement(
1719
2094
  "button",
1720
2095
  {
1721
- role: "menuitem",
2096
+ key,
1722
2097
  type: "button",
1723
- onClick: () => switchTo(m.tenantId),
2098
+ "data-testid": `scope-switcher-option-${m.scopeType}-${m.scopeId}`,
2099
+ disabled: busy === key,
2100
+ onClick: async () => {
2101
+ setBusy(key);
2102
+ setSwitchError(null);
2103
+ try {
2104
+ await switchScope({ type: m.scopeType, id: m.scopeId });
2105
+ setOpen(false);
2106
+ onSwitched?.({ type: m.scopeType, id: m.scopeId });
2107
+ } catch (err) {
2108
+ setSwitchError(err.message || "Scope switch failed");
2109
+ } finally {
2110
+ setBusy(null);
2111
+ }
2112
+ },
1724
2113
  style: {
1725
2114
  display: "block",
1726
2115
  width: "100%",
1727
2116
  textAlign: "left",
1728
2117
  padding: "8px 10px",
1729
- background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
1730
2118
  border: "none",
2119
+ background: isActive ? "#f3f4f6" : "transparent",
2120
+ cursor: busy ? "wait" : "pointer",
1731
2121
  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
- ]
2122
+ fontSize: 13
2123
+ }
1740
2124
  },
1741
- m.tenantId
1742
- )) }) : null
1743
- ]
1744
- }
2125
+ createElement("div", { style: { fontWeight: 500 } }, m.scopeName),
2126
+ createElement(
2127
+ "div",
2128
+ { style: { fontSize: 11, opacity: 0.6 } },
2129
+ `${m.scopeType} \xB7 ${m.role}${m.grantedVia && m.grantedVia !== "direct" ? ` \xB7 via ${m.grantedVia}` : ""}`
2130
+ )
2131
+ );
2132
+ })
2133
+ ) : null
1745
2134
  );
1746
2135
  }
1747
2136
  function useImpersonation() {
@@ -2354,10 +2743,13 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
2354
2743
  const branding = useResolvedSdkBranding(iqAuthBaseUrl);
2355
2744
  const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
2356
2745
  const accent = branding?.accentColor || "#6366f1";
2746
+ const t2 = useT();
2747
+ const { manager } = useCtx();
2357
2748
  const [memberships, setMemberships] = useState([]);
2358
2749
  const [activeTenantId, setActiveTenantId] = useState(null);
2359
2750
  const [loading, setLoading] = useState(true);
2360
2751
  const [error, setError] = useState(null);
2752
+ const [pendingStep, setPendingStep] = useState(null);
2361
2753
  useEffect(() => {
2362
2754
  let cancelled = false;
2363
2755
  Promise.all([
@@ -2381,21 +2773,81 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
2381
2773
  onSelect?.(tenantId);
2382
2774
  return;
2383
2775
  }
2776
+ setError(null);
2777
+ setPendingStep(null);
2384
2778
  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
- });
2779
+ const result = await performTenantSwitch(manager, iqAuthBaseUrl, tenantId);
2780
+ if (result.kind === "mfa_required") {
2781
+ setPendingStep({
2782
+ kind: "mfa_required",
2783
+ tenantId: result.tenantId,
2784
+ mfaChallengeToken: result.mfaChallengeToken,
2785
+ availableMethods: result.availableMethods
2786
+ });
2787
+ return;
2788
+ }
2789
+ if (result.kind === "scope_selection_required") {
2790
+ setPendingStep({
2791
+ kind: "scope_selection_required",
2792
+ tenantId: result.tenantId,
2793
+ scopeSelectionToken: result.scopeSelectionToken
2794
+ });
2795
+ return;
2796
+ }
2391
2797
  setActiveTenantId(tenantId);
2392
2798
  onSelect?.(tenantId);
2393
2799
  } catch (err) {
2394
2800
  setError(err instanceof Error ? err.message : "Failed to switch organization");
2395
2801
  }
2396
2802
  };
2803
+ const hostedSignInUrl = () => {
2804
+ const baseHosted = `${baseUrl}/sign-in`;
2805
+ if (!pendingStep) return baseHosted;
2806
+ const params = new URLSearchParams();
2807
+ if (pendingStep.kind === "mfa_required") {
2808
+ params.set("mfaChallengeToken", pendingStep.mfaChallengeToken);
2809
+ if (pendingStep.availableMethods.length) {
2810
+ params.set("availableMethods", pendingStep.availableMethods.join(","));
2811
+ }
2812
+ } else {
2813
+ params.set("prompt", "login");
2814
+ }
2815
+ return `${baseHosted}?${params.toString()}`;
2816
+ };
2397
2817
  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
2818
  error ? /* @__PURE__ */ jsx(ErrorBanner, { message: error }) : null,
2819
+ pendingStep ? /* @__PURE__ */ jsxs(
2820
+ "div",
2821
+ {
2822
+ "data-testid": pendingStep.kind === "mfa_required" ? "prompt-org-list-mfa-required" : "prompt-org-list-scope-required",
2823
+ role: "alert",
2824
+ style: {
2825
+ padding: "10px 12px",
2826
+ background: `${accent}10`,
2827
+ border: `1px solid ${accent}55`,
2828
+ borderRadius: 6
2829
+ },
2830
+ children: [
2831
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a", marginBottom: 4 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredTitle") : t2("orgSwitcher.scopeSelectionRequiredTitle") }),
2832
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#334155", marginBottom: 8 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredBody") : t2("orgSwitcher.scopeSelectionRequiredBody") }),
2833
+ /* @__PURE__ */ jsx(
2834
+ "a",
2835
+ {
2836
+ href: hostedSignInUrl(),
2837
+ "data-testid": "link-org-list-continue-hosted",
2838
+ style: {
2839
+ display: "inline-block",
2840
+ fontSize: 12,
2841
+ fontWeight: 600,
2842
+ color: branding?.accentColor || accent,
2843
+ textDecoration: "none"
2844
+ },
2845
+ children: t2("orgSwitcher.continueInHostedSignIn")
2846
+ }
2847
+ )
2848
+ ]
2849
+ }
2850
+ ) : null,
2399
2851
  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
2852
  const active = m.tenantId === activeTenantId;
2401
2853
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
@@ -2433,8 +2885,8 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
2433
2885
  iqAuthBaseUrl,
2434
2886
  unstyled: true,
2435
2887
  appearance,
2436
- onCreated: (t2) => {
2437
- onSelect?.(t2.id);
2888
+ onCreated: (t3) => {
2889
+ onSelect?.(t3.id);
2438
2890
  reloadList();
2439
2891
  },
2440
2892
  redirectUrl: createRedirectUrl
@@ -2670,8 +3122,6 @@ function LinkedAccounts({ className, onChange, ...rest }) {
2670
3122
  var __version__ = "phase-bc-1.0.0";
2671
3123
 
2672
3124
  export {
2673
- sanitizeReturnTo,
2674
- isReturnToAllowed,
2675
3125
  IQAuthProvider,
2676
3126
  __useIQAuthInternal,
2677
3127
  useLocale,
@@ -2691,6 +3141,9 @@ export {
2691
3141
  IQAuthLoading,
2692
3142
  IQAuthLoaded,
2693
3143
  RedirectToSignIn,
3144
+ performScopeSwitch,
3145
+ performTenantSwitch,
3146
+ claimSatisfiesScope,
2694
3147
  Protect,
2695
3148
  RedirectToSignedIn,
2696
3149
  useReturnTo,
@@ -2701,11 +3154,14 @@ export {
2701
3154
  sanitizeBrandCss,
2702
3155
  useResolvedSdkBranding,
2703
3156
  isSilentSsoEligible,
3157
+ resolveAfterSignInDestination,
2704
3158
  SignIn,
2705
3159
  SignUp,
2706
3160
  UserButton,
2707
3161
  UserProfile,
2708
3162
  OrganizationSwitcher,
3163
+ useMemberships,
3164
+ ScopeSwitcher,
2709
3165
  useImpersonation,
2710
3166
  ImpersonationBanner,
2711
3167
  useReverification,