@iqauth/sdk 2.6.4 → 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.
- package/README.md +173 -1
- package/dist/browser-session.d.mts +4 -4
- package/dist/browser-session.d.ts +4 -4
- package/dist/browser-session.js +212 -46
- package/dist/browser-session.mjs +3 -3
- package/dist/browser.d.mts +5 -5
- package/dist/browser.d.ts +5 -5
- package/dist/browser.js +293 -34
- package/dist/browser.mjs +5 -5
- package/dist/{chunk-BVV54LPI.mjs → chunk-25SSYDIP.mjs} +10 -4
- package/dist/{chunk-XAWYUPMO.mjs → chunk-4V7FKOTG.mjs} +242 -22
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
- package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
- package/dist/chunk-GLXSIGVS.mjs +66 -0
- package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
- package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
- package/dist/chunk-JRDVUWAL.mjs +46 -0
- package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
- package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
- package/dist/chunk-VYQ3ETCK.mjs +244 -0
- package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
- package/dist/chunk-WHT6WKTY.mjs +3180 -0
- package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
- package/dist/chunk-WSH4SW7F.mjs +490 -0
- package/dist/{chunk-W3F4JYGP.mjs → chunk-ZLJPABB7.mjs} +139 -23
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{client-BNQe3AgF.d.ts → client-D8L-PaWr.d.mts} +59 -6
- package/dist/{client-kYlJFgPv.d.mts → client-DkPL0EPZ.d.ts} +59 -6
- package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
- package/dist/errors-Jl1Jtm-6.d.mts +107 -0
- package/dist/errors-Jl1Jtm-6.d.ts +107 -0
- package/dist/{express-CHpfa7D_.d.ts → express-Budysq4h.d.ts} +2 -2
- package/dist/{express-B6_1vBYZ.d.mts → express-DDTA3qV1.d.mts} +2 -2
- package/dist/express.d.mts +7 -6
- package/dist/express.d.ts +7 -6
- package/dist/express.js +563 -85
- package/dist/express.mjs +73 -34
- package/dist/fastify.d.mts +10 -0
- package/dist/fastify.d.ts +10 -0
- package/dist/fastify.js +589 -65
- package/dist/fastify.mjs +101 -11
- package/dist/hono.d.mts +10 -0
- package/dist/hono.d.ts +10 -0
- package/dist/hono.js +566 -65
- package/dist/hono.mjs +78 -11
- package/dist/index-Cko-d5po.d.mts +1848 -0
- package/dist/index-RNqwEcmY.d.ts +1848 -0
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +694 -75
- package/dist/index.mjs +30 -10
- package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
- package/dist/locales.d.mts +1 -1
- package/dist/locales.d.ts +1 -1
- package/dist/locales.js +36 -0
- package/dist/locales.mjs +1 -1
- package/dist/mobile.d.mts +77 -7
- package/dist/mobile.d.ts +77 -7
- package/dist/mobile.js +307 -46
- package/dist/mobile.mjs +98 -3
- package/dist/next.d.mts +10 -1
- package/dist/next.d.ts +10 -1
- package/dist/next.js +596 -205
- package/dist/next.mjs +83 -10
- package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
- package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
- package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
- package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
- package/dist/react-permissions.d.mts +52 -0
- package/dist/react-permissions.d.ts +52 -0
- package/dist/react-permissions.js +239 -0
- package/dist/react-permissions.mjs +98 -0
- package/dist/react.d.mts +9 -1624
- package/dist/react.d.ts +9 -1624
- package/dist/react.js +882 -73
- package/dist/react.mjs +71 -2631
- package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
- package/dist/server/handlers.d.mts +200 -4
- package/dist/server/handlers.d.ts +200 -4
- package/dist/server/handlers.js +530 -16
- package/dist/server/handlers.mjs +14 -3
- package/dist/server.d.mts +171 -8
- package/dist/server.d.ts +171 -8
- package/dist/server.js +579 -61
- package/dist/server.mjs +99 -12
- package/dist/service.d.mts +4 -4
- package/dist/service.d.ts +4 -4
- package/dist/service.js +212 -46
- package/dist/service.mjs +3 -3
- package/dist/{signIn-CiIBTJIh.d.mts → signIn-CReqfXsh.d.mts} +95 -3
- package/dist/{signIn-OCr88Zf8.d.ts → signIn-Cfa1GTpO.d.ts} +95 -3
- package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
- package/dist/test.mjs +3 -3
- package/dist/{tokens-DCyzzn8L.d.mts → tokens-9F6ETrzk.d.ts} +9 -2
- package/dist/{tokens-aHiGFr_E.d.ts → tokens-B06VtvUi.d.mts} +9 -2
- package/dist/{types-DZAflmmq.d.mts → types-Bn8O-OEd.d.mts} +164 -11
- package/dist/{types-DZAflmmq.d.ts → types-Bn8O-OEd.d.ts} +164 -11
- package/dist/{types-6bNdxesb.d.ts → types-DnU2LhXR.d.mts} +7 -1
- package/dist/{types-6bNdxesb.d.mts → types-DnU2LhXR.d.ts} +7 -1
- package/dist/webhooks.d.mts +113 -17
- package/dist/webhooks.d.ts +113 -17
- package/dist/webhooks.js +179 -15
- package/dist/webhooks.mjs +7 -1
- package/dist/ws.d.mts +2 -2
- package/dist/ws.d.ts +2 -2
- package/dist/ws.js +80 -30
- package/dist/ws.mjs +4 -4
- package/docs/error-handling.md +101 -0
- package/docs/guides/effective-permissions.md +171 -0
- package/docs/guides/invitations.md +65 -0
- package/package.json +19 -4
- package/dist/chunk-6TDJJER7.mjs +0 -217
- package/dist/chunk-UKZLOHZG.mjs +0 -83
- package/dist/errors-CDdl24MP.d.mts +0 -52
- package/dist/errors-CDdl24MP.d.ts +0 -52
package/dist/react.js
CHANGED
|
@@ -25,13 +25,30 @@ var IQAuthError;
|
|
|
25
25
|
var init_errors = __esm({
|
|
26
26
|
"src/errors.ts"() {
|
|
27
27
|
"use strict";
|
|
28
|
-
IQAuthError = class extends Error {
|
|
29
|
-
constructor(code, message, status,
|
|
28
|
+
IQAuthError = class _IQAuthError extends Error {
|
|
29
|
+
constructor(code, message, status, cause) {
|
|
30
30
|
super(message);
|
|
31
31
|
this.name = "IQAuthError";
|
|
32
32
|
this.code = code;
|
|
33
33
|
this.status = status;
|
|
34
|
-
this.
|
|
34
|
+
this.cause = cause;
|
|
35
|
+
this.raw = cause;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
39
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
40
|
+
*/
|
|
41
|
+
static isIQAuthError(value) {
|
|
42
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Type-narrowed code check. Lets callers write
|
|
46
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
47
|
+
* taxonomy without losing the ability to handle server codes via
|
|
48
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
49
|
+
*/
|
|
50
|
+
is(code) {
|
|
51
|
+
return this.code === code;
|
|
35
52
|
}
|
|
36
53
|
};
|
|
37
54
|
}
|
|
@@ -177,7 +194,7 @@ async function buildSignInUrl(manager, opts = {}) {
|
|
|
177
194
|
returnTo,
|
|
178
195
|
createdAt: Date.now()
|
|
179
196
|
});
|
|
180
|
-
const url2 = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.
|
|
197
|
+
const url2 = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.hostedIssuerUrl);
|
|
181
198
|
url2.searchParams.set("response_type", "code");
|
|
182
199
|
url2.searchParams.set("app", manager.appKey);
|
|
183
200
|
url2.searchParams.set("publishable_key", manager.publishableKey.raw);
|
|
@@ -192,33 +209,50 @@ async function buildSignInUrl(manager, opts = {}) {
|
|
|
192
209
|
return url2.toString();
|
|
193
210
|
}
|
|
194
211
|
async function redirectToSignIn(manager, opts = {}) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
const t0 = Date.now();
|
|
213
|
+
let ok = false;
|
|
214
|
+
let code;
|
|
215
|
+
try {
|
|
216
|
+
const url2 = await buildSignInUrl(manager, opts);
|
|
217
|
+
if (typeof window === "undefined") {
|
|
218
|
+
code = "NO_WINDOW";
|
|
219
|
+
throw new Error("redirectToSignIn requires a browser environment");
|
|
220
|
+
}
|
|
221
|
+
ok = true;
|
|
222
|
+
manager.recordTiming("signIn", Date.now() - t0, true);
|
|
223
|
+
window.location.assign(url2);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (!ok) manager.recordTiming("signIn", Date.now() - t0, false, code ?? (err instanceof Error ? err.message : "ERROR"));
|
|
226
|
+
throw err;
|
|
198
227
|
}
|
|
199
|
-
window.location.assign(url2);
|
|
200
228
|
}
|
|
201
229
|
async function signIn(manager, opts = {}) {
|
|
202
230
|
return redirectToSignIn(manager, opts);
|
|
203
231
|
}
|
|
204
232
|
async function handleAuthCallback(manager, options = {}) {
|
|
233
|
+
const t0 = Date.now();
|
|
234
|
+
const emit = (ok, code2) => manager.recordTiming("signIn", Date.now() - t0, ok, code2);
|
|
205
235
|
const url2 = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
|
|
206
236
|
const code = url2.searchParams.get("code");
|
|
207
237
|
const state = url2.searchParams.get("state");
|
|
208
238
|
const errorParam = url2.searchParams.get("error");
|
|
209
239
|
if (errorParam) {
|
|
240
|
+
emit(false, errorParam);
|
|
210
241
|
return { ok: false, returnTo: "/", error: errorParam };
|
|
211
242
|
}
|
|
212
243
|
if (!code || !state) {
|
|
244
|
+
emit(false, "missing_code_or_state");
|
|
213
245
|
return { ok: false, returnTo: "/", error: "missing_code_or_state" };
|
|
214
246
|
}
|
|
215
247
|
const record = loadPkce(state);
|
|
216
248
|
if (!record) {
|
|
249
|
+
emit(false, "unknown_state");
|
|
217
250
|
return { ok: false, returnTo: "/", error: "unknown_state" };
|
|
218
251
|
}
|
|
219
252
|
clearPkce(state);
|
|
220
253
|
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
|
221
254
|
if (!fetchImpl) {
|
|
255
|
+
emit(false, "no_fetch");
|
|
222
256
|
return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
|
|
223
257
|
}
|
|
224
258
|
const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
|
|
@@ -237,10 +271,12 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
237
271
|
const body = await res.json().catch(() => ({}));
|
|
238
272
|
if (!res.ok) {
|
|
239
273
|
const desc = body.error_description ?? body.error ?? "token_exchange_failed";
|
|
274
|
+
emit(false, desc);
|
|
240
275
|
return { ok: false, returnTo: record.returnTo, error: desc };
|
|
241
276
|
}
|
|
242
277
|
const tokens = body;
|
|
243
278
|
if (!tokens.access_token) {
|
|
279
|
+
emit(false, "missing_access_token");
|
|
244
280
|
return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
|
|
245
281
|
}
|
|
246
282
|
if (tokens.refresh_token) {
|
|
@@ -248,21 +284,24 @@ async function handleAuthCallback(manager, options = {}) {
|
|
|
248
284
|
setCookie(cookieName, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
|
|
249
285
|
}
|
|
250
286
|
manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
|
|
287
|
+
emit(true);
|
|
251
288
|
return { ok: true, returnTo: record.returnTo };
|
|
252
289
|
}
|
|
253
290
|
async function signOut(manager, opts = {}) {
|
|
254
291
|
if (!opts.localOnly) {
|
|
255
292
|
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
293
|
+
const idempotency = manager.getIdempotencyToken();
|
|
256
294
|
try {
|
|
257
295
|
const url2 = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
258
|
-
await manager.fetch(url2, { method: "POST" }).catch(() => void 0);
|
|
296
|
+
await manager.fetch(url2, { method: "POST", headers: { "X-IQAuth-Idempotency": idempotency } }).catch(() => void 0);
|
|
259
297
|
} catch {
|
|
260
298
|
}
|
|
261
299
|
if (opts.endSsoSession !== false) {
|
|
262
300
|
try {
|
|
263
301
|
await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
|
|
264
302
|
method: "POST",
|
|
265
|
-
credentials: "include"
|
|
303
|
+
credentials: "include",
|
|
304
|
+
headers: { "X-IQAuth-Idempotency": idempotency }
|
|
266
305
|
}).catch(() => void 0);
|
|
267
306
|
} catch {
|
|
268
307
|
}
|
|
@@ -782,6 +821,7 @@ __export(react_exports, {
|
|
|
782
821
|
Protect: () => Protect,
|
|
783
822
|
RedirectToSignIn: () => RedirectToSignIn,
|
|
784
823
|
RedirectToSignedIn: () => RedirectToSignedIn,
|
|
824
|
+
ScopeSwitcher: () => ScopeSwitcher,
|
|
785
825
|
SignIn: () => SignIn,
|
|
786
826
|
SignUp: () => SignUp,
|
|
787
827
|
SignedIn: () => SignedIn,
|
|
@@ -789,10 +829,15 @@ __export(react_exports, {
|
|
|
789
829
|
UserButton: () => UserButton,
|
|
790
830
|
UserProfile: () => UserProfile,
|
|
791
831
|
Waitlist: () => Waitlist,
|
|
832
|
+
__useIQAuthInternal: () => __useIQAuthInternal,
|
|
792
833
|
__version__: () => __version__,
|
|
834
|
+
claimSatisfiesScope: () => claimSatisfiesScope,
|
|
793
835
|
isReturnToAllowed: () => isReturnToAllowed,
|
|
794
836
|
isSilentSsoEligible: () => isSilentSsoEligible,
|
|
837
|
+
performScopeSwitch: () => performScopeSwitch,
|
|
838
|
+
performTenantSwitch: () => performTenantSwitch,
|
|
795
839
|
preflightReturnTo: () => preflightReturnTo,
|
|
840
|
+
resolveAfterSignInDestination: () => resolveAfterSignInDestination,
|
|
796
841
|
revokeSession: () => revokeSession,
|
|
797
842
|
sanitizeBrandCss: () => sanitizeBrandCss,
|
|
798
843
|
sanitizeReturnTo: () => sanitizeReturnTo,
|
|
@@ -806,6 +851,7 @@ __export(react_exports, {
|
|
|
806
851
|
useLinkedIdentities: () => useLinkedIdentities,
|
|
807
852
|
useLocale: () => useLocale,
|
|
808
853
|
useMagicLink: () => useMagicLink,
|
|
854
|
+
useMemberships: () => useMemberships,
|
|
809
855
|
useOrganization: () => useOrganization,
|
|
810
856
|
usePasskey: () => usePasskey,
|
|
811
857
|
useResolvedSdkBranding: () => useResolvedSdkBranding,
|
|
@@ -854,14 +900,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
854
900
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
855
901
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
856
902
|
throw new IQAuthError(
|
|
857
|
-
"
|
|
903
|
+
"config_invalid",
|
|
858
904
|
`${ctx}IQAuth publishable key is missing. Set IQAUTH_PUBLISHABLE_KEY (or pass publishableKey) to a pk_test_\u2026 or pk_live_\u2026 value from the IQAuth admin console.`
|
|
859
905
|
);
|
|
860
906
|
}
|
|
861
907
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
862
908
|
if (!shapeMatch) {
|
|
863
909
|
throw new IQAuthError(
|
|
864
|
-
"
|
|
910
|
+
"config_invalid",
|
|
865
911
|
`${ctx}IQAuth publishable key is malformed (got ${raw.slice(0, 12)}\u2026). Expected pk_test_\u2026 or pk_live_\u2026; regenerate the key from the IQAuth admin console.`
|
|
866
912
|
);
|
|
867
913
|
}
|
|
@@ -870,19 +916,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
870
916
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
871
917
|
} catch {
|
|
872
918
|
throw new IQAuthError(
|
|
873
|
-
"
|
|
919
|
+
"config_invalid",
|
|
874
920
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
875
921
|
);
|
|
876
922
|
}
|
|
877
923
|
if (!isPublishableKeyPayload(decoded)) {
|
|
878
924
|
throw new IQAuthError(
|
|
879
|
-
"
|
|
925
|
+
"config_invalid",
|
|
880
926
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
881
927
|
);
|
|
882
928
|
}
|
|
883
929
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
884
930
|
throw new IQAuthError(
|
|
885
|
-
"
|
|
931
|
+
"config_invalid",
|
|
886
932
|
`${ctx}IQAuth publishable key encodes an invalid issuer (iss=${JSON.stringify(decoded.iss)}). Expected a fully-qualified URL like "https://auth.example.com" (scheme required). Regenerate the key from the IQAuth admin console \u2014 the new key will encode a valid issuer URL.`
|
|
887
933
|
);
|
|
888
934
|
}
|
|
@@ -896,6 +942,7 @@ function isPublishableKeyPayload(value) {
|
|
|
896
942
|
|
|
897
943
|
// src/browser/sessionManager.ts
|
|
898
944
|
init_storage();
|
|
945
|
+
var PROBE_WAIT_MS = 80;
|
|
899
946
|
var DEFAULT_REFRESH_PATH = "/api/v1/auth/refresh";
|
|
900
947
|
var DEFAULT_USERINFO_PATH = "/api/v1/auth/me";
|
|
901
948
|
async function readAuthErrorCode(res) {
|
|
@@ -933,7 +980,16 @@ function claimsToSessionUser(claims) {
|
|
|
933
980
|
tenantId: claims.tenantId,
|
|
934
981
|
vendorId: claims.vendorId,
|
|
935
982
|
roles: claims.roles ?? [],
|
|
936
|
-
entitlements: claims.entitlements ?? []
|
|
983
|
+
entitlements: claims.entitlements ?? [],
|
|
984
|
+
// SDK 2.7.0 (Task #124) — pass through identity claims when issued.
|
|
985
|
+
...claims.picture !== void 0 ? { picture: claims.picture } : {},
|
|
986
|
+
...claims.email_verified !== void 0 ? { emailVerified: claims.email_verified } : {},
|
|
987
|
+
...claims.given_name !== void 0 ? { givenName: claims.given_name } : {},
|
|
988
|
+
...claims.family_name !== void 0 ? { familyName: claims.family_name } : {},
|
|
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 } : {}
|
|
937
993
|
};
|
|
938
994
|
}
|
|
939
995
|
var EMPTY = {
|
|
@@ -957,11 +1013,50 @@ var NO_OP_STORE = {
|
|
|
957
1013
|
write: () => void 0,
|
|
958
1014
|
clear: () => void 0
|
|
959
1015
|
};
|
|
1016
|
+
var IDEMPOTENCY_HEADER = "X-IQAuth-Idempotency";
|
|
1017
|
+
function randomIdempotencyToken() {
|
|
1018
|
+
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
1019
|
+
const bytes = new Uint8Array(16);
|
|
1020
|
+
crypto.getRandomValues(bytes);
|
|
1021
|
+
let out = "";
|
|
1022
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1023
|
+
return out;
|
|
1024
|
+
}
|
|
1025
|
+
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
1026
|
+
}
|
|
960
1027
|
var SessionManager = class {
|
|
961
1028
|
constructor(options) {
|
|
962
1029
|
this.snapshot = { ...EMPTY };
|
|
963
1030
|
this.listeners = /* @__PURE__ */ new Set();
|
|
964
1031
|
this.refreshPromise = null;
|
|
1032
|
+
/**
|
|
1033
|
+
* Cancellation handle for the in-flight refresh, if any. `signOut()` (or a
|
|
1034
|
+
* `session:signout` broadcast from another tab) calls `abort()` so the
|
|
1035
|
+
* refresh response is dropped before it can write a fresh access cookie
|
|
1036
|
+
* on top of the just-cleared session — the second root cause of "ghost
|
|
1037
|
+
* signed-in" sessions after Sign Out.
|
|
1038
|
+
*/
|
|
1039
|
+
this.refreshAbort = null;
|
|
1040
|
+
/**
|
|
1041
|
+
* Set to `true` by `signOut()` / `signOutLocal()` for the lifetime of the
|
|
1042
|
+
* call. Used as a safety belt: even if a refresh response arrives while
|
|
1043
|
+
* `refreshAbort` was unable to interrupt the network call (e.g. the body
|
|
1044
|
+
* was already streaming back), `runRefresh` checks this flag before
|
|
1045
|
+
* mutating session state and bails out.
|
|
1046
|
+
*/
|
|
1047
|
+
this.signoutInProgress = false;
|
|
1048
|
+
/**
|
|
1049
|
+
* Per-session opaque idempotency token. Sent as `X-IQAuth-Idempotency` on
|
|
1050
|
+
* every /refresh and /signout request the SDK makes through a framework
|
|
1051
|
+
* adapter (Express/Fastify/Hono/Next), so the adapter's `SignoutRegistry`
|
|
1052
|
+
* can collapse a refresh that lands moments after a signout — even when
|
|
1053
|
+
* the two requests are routed to different server instances (multi-replica
|
|
1054
|
+
* deployments).
|
|
1055
|
+
*
|
|
1056
|
+
* Generated lazily on first use, rotated on signout so the next session
|
|
1057
|
+
* starts with a fresh token. Opaque random — never the raw refresh token.
|
|
1058
|
+
*/
|
|
1059
|
+
this.idempotencyToken = null;
|
|
965
1060
|
this.channel = null;
|
|
966
1061
|
this.proactiveTimer = null;
|
|
967
1062
|
this.bootstrapped = false;
|
|
@@ -969,6 +1064,8 @@ var SessionManager = class {
|
|
|
969
1064
|
this.remoteRefreshWaiters = [];
|
|
970
1065
|
/** Active claims by other tabs (keyed by source tabId). */
|
|
971
1066
|
this.foreignClaim = null;
|
|
1067
|
+
/** Resolver for an in-flight cross-tab `session:probe`, set during bootstrap. */
|
|
1068
|
+
this.probeResolver = null;
|
|
972
1069
|
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
|
|
973
1070
|
this.key = parsed;
|
|
974
1071
|
const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
|
|
@@ -981,6 +1078,8 @@ var SessionManager = class {
|
|
|
981
1078
|
this.refreshCookieName = options.cookieNames?.refresh ?? REFRESH_COOKIE;
|
|
982
1079
|
this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore(this.refreshCookieName) : NO_OP_STORE);
|
|
983
1080
|
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
1081
|
+
this.debug = options.debug ?? false;
|
|
1082
|
+
this.onTimingEvent = options.onTimingEvent ?? null;
|
|
984
1083
|
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
985
1084
|
throw new Error("global fetch is not available; pass fetchImpl");
|
|
986
1085
|
}));
|
|
@@ -1007,10 +1106,35 @@ var SessionManager = class {
|
|
|
1007
1106
|
get issuerUrl() {
|
|
1008
1107
|
return this.issuer;
|
|
1009
1108
|
}
|
|
1109
|
+
/**
|
|
1110
|
+
* SDK 2.7.0 (Task #124) — The hosted IQAuth host derived from the
|
|
1111
|
+
* publishable key's `iss` claim, normalized to URL form. This is what
|
|
1112
|
+
* `<SignIn/>` and `buildSignInUrl` use to talk to the hosted UI; it
|
|
1113
|
+
* deliberately ignores the `issuer` constructor override so a misrouted
|
|
1114
|
+
* `issuer` (e.g. pointed at the consumer app's own domain) cannot break
|
|
1115
|
+
* the hosted flow. Use {@link issuerUrl} for token / discovery endpoints.
|
|
1116
|
+
*/
|
|
1117
|
+
get hostedIssuerUrl() {
|
|
1118
|
+
const iss = this.key.iss;
|
|
1119
|
+
return (iss.startsWith("http") ? iss : `https://${iss}`).replace(/\/+$/, "");
|
|
1120
|
+
}
|
|
1010
1121
|
/** Cookie name the SDK uses for the refresh token (overridable via `cookieNames.refresh`). */
|
|
1011
1122
|
get refreshCookie() {
|
|
1012
1123
|
return this.refreshCookieName;
|
|
1013
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Returns the current per-session idempotency token, generating one
|
|
1127
|
+
* lazily on first use. Sent as the `X-IQAuth-Idempotency` header on
|
|
1128
|
+
* /refresh and /signout requests so the framework adapter's
|
|
1129
|
+
* `SignoutRegistry` can collapse a refresh-vs-signout race even across
|
|
1130
|
+
* server instances.
|
|
1131
|
+
*/
|
|
1132
|
+
getIdempotencyToken() {
|
|
1133
|
+
if (!this.idempotencyToken) {
|
|
1134
|
+
this.idempotencyToken = randomIdempotencyToken();
|
|
1135
|
+
}
|
|
1136
|
+
return this.idempotencyToken;
|
|
1137
|
+
}
|
|
1014
1138
|
getSnapshot() {
|
|
1015
1139
|
return this.snapshot;
|
|
1016
1140
|
}
|
|
@@ -1024,9 +1148,44 @@ var SessionManager = class {
|
|
|
1024
1148
|
* One-time bootstrap: warm the session from the refresh cookie if present.
|
|
1025
1149
|
* Safe to call multiple times.
|
|
1026
1150
|
*/
|
|
1151
|
+
/**
|
|
1152
|
+
* Task #126: Public timing-event emitter. Used by the browser sign-in
|
|
1153
|
+
* helpers (redirectToSignIn / handleAuthCallback) to surface signIn-phase
|
|
1154
|
+
* timings through the same `debug` + `onTimingEvent` channel as
|
|
1155
|
+
* bootstrap/refresh. Safe to call from anywhere — internal callers
|
|
1156
|
+
* pre-compute durationMs.
|
|
1157
|
+
*/
|
|
1158
|
+
recordTiming(phase, durationMs, ok, code) {
|
|
1159
|
+
this.emitTiming(phase, durationMs, ok, code);
|
|
1160
|
+
}
|
|
1161
|
+
/** Task #126: emit a session timing event to debug log + onTimingEvent hook. */
|
|
1162
|
+
emitTiming(phase, durationMs, ok, code) {
|
|
1163
|
+
if (this.debug) {
|
|
1164
|
+
try {
|
|
1165
|
+
console.debug("[iqauth_session]", { phase, durationMs, ok, code });
|
|
1166
|
+
} catch {
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (this.onTimingEvent) {
|
|
1170
|
+
try {
|
|
1171
|
+
this.onTimingEvent({ phase, durationMs, ok, code });
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1027
1176
|
async bootstrap() {
|
|
1028
1177
|
if (this.bootstrapped) return;
|
|
1029
1178
|
this.bootstrapped = true;
|
|
1179
|
+
const t0 = Date.now();
|
|
1180
|
+
try {
|
|
1181
|
+
await this.bootstrapInner();
|
|
1182
|
+
this.emitTiming("bootstrap", Date.now() - t0, this.snapshot.status === "authenticated");
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
this.emitTiming("bootstrap", Date.now() - t0, false, err instanceof Error ? err.message : "ERROR");
|
|
1185
|
+
throw err;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async bootstrapInner() {
|
|
1030
1189
|
if (this.serverManagedSession) {
|
|
1031
1190
|
try {
|
|
1032
1191
|
const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
|
|
@@ -1061,6 +1220,15 @@ var SessionManager = class {
|
|
|
1061
1220
|
return;
|
|
1062
1221
|
}
|
|
1063
1222
|
}
|
|
1223
|
+
const peerSnapshot = await this.probePeers();
|
|
1224
|
+
if (peerSnapshot && peerSnapshot.status === "authenticated") {
|
|
1225
|
+
this.update({
|
|
1226
|
+
...peerSnapshot,
|
|
1227
|
+
version: Math.max(this.snapshot.version, peerSnapshot.version) + 1
|
|
1228
|
+
});
|
|
1229
|
+
this.scheduleProactiveRefresh();
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1064
1232
|
const stored = await Promise.resolve(this.tokenStore.read());
|
|
1065
1233
|
if (!stored) {
|
|
1066
1234
|
this.setStatus("unauthenticated");
|
|
@@ -1069,6 +1237,22 @@ var SessionManager = class {
|
|
|
1069
1237
|
const ok = await this.refresh();
|
|
1070
1238
|
if (!ok) this.setStatus("unauthenticated");
|
|
1071
1239
|
}
|
|
1240
|
+
probePeers() {
|
|
1241
|
+
if (!this.channel) return Promise.resolve(null);
|
|
1242
|
+
return new Promise((resolve) => {
|
|
1243
|
+
let settled = false;
|
|
1244
|
+
const finish = (snap) => {
|
|
1245
|
+
if (settled) return;
|
|
1246
|
+
settled = true;
|
|
1247
|
+
this.probeResolver = null;
|
|
1248
|
+
clearTimeout(timer);
|
|
1249
|
+
resolve(snap);
|
|
1250
|
+
};
|
|
1251
|
+
this.probeResolver = (snap) => finish(snap);
|
|
1252
|
+
const timer = setTimeout(() => finish(null), PROBE_WAIT_MS);
|
|
1253
|
+
this.broadcastEnvelope({ type: "session:probe", source: this.tabId, ts: Date.now() });
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1072
1256
|
/**
|
|
1073
1257
|
* Single-flight token refresh, coordinated across tabs via BroadcastChannel.
|
|
1074
1258
|
*
|
|
@@ -1082,30 +1266,48 @@ var SessionManager = class {
|
|
|
1082
1266
|
*/
|
|
1083
1267
|
refresh() {
|
|
1084
1268
|
if (this.refreshPromise) return this.refreshPromise;
|
|
1085
|
-
|
|
1269
|
+
const t0 = Date.now();
|
|
1270
|
+
this.refreshPromise = this.runRefresh().then((ok) => {
|
|
1271
|
+
this.emitTiming("refresh", Date.now() - t0, ok, ok ? void 0 : this.snapshot.error?.code ?? "REFRESH_FAILED");
|
|
1272
|
+
return ok;
|
|
1273
|
+
}).finally(() => {
|
|
1086
1274
|
this.refreshPromise = null;
|
|
1087
1275
|
});
|
|
1088
1276
|
return this.refreshPromise;
|
|
1089
1277
|
}
|
|
1090
1278
|
async runRefresh() {
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
|
|
1094
|
-
await new Promise((r) => setTimeout(r, 25));
|
|
1095
|
-
const foreign = this.foreignClaim;
|
|
1096
|
-
if (foreign && this.claimWins(foreign, myClaim)) {
|
|
1097
|
-
return this.waitForForeignRefresh();
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1279
|
+
const abort = new AbortController();
|
|
1280
|
+
this.refreshAbort = abort;
|
|
1100
1281
|
try {
|
|
1282
|
+
const myClaim = { source: this.tabId, ts: Date.now() };
|
|
1283
|
+
if (this.channel) {
|
|
1284
|
+
this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
|
|
1285
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
1286
|
+
if (abort.signal.aborted || this.signoutInProgress) {
|
|
1287
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
const foreign = this.foreignClaim;
|
|
1291
|
+
if (foreign && this.claimWins(foreign, myClaim)) {
|
|
1292
|
+
return this.waitForForeignRefresh();
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1101
1295
|
const refreshToken = await Promise.resolve(this.tokenStore.read());
|
|
1102
1296
|
const res = await this.fetchImpl(`${this.issuer}${this.refreshPath}`, {
|
|
1103
1297
|
method: "POST",
|
|
1104
1298
|
credentials: "include",
|
|
1105
|
-
headers: {
|
|
1106
|
-
|
|
1299
|
+
headers: {
|
|
1300
|
+
"Content-Type": "application/json",
|
|
1301
|
+
[IDEMPOTENCY_HEADER]: this.getIdempotencyToken()
|
|
1302
|
+
},
|
|
1303
|
+
body: JSON.stringify(refreshToken ? { refreshToken } : {}),
|
|
1304
|
+
signal: abort.signal
|
|
1107
1305
|
});
|
|
1108
1306
|
const body = await res.json().catch(() => ({}));
|
|
1307
|
+
if (this.signoutInProgress || abort.signal.aborted) {
|
|
1308
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1109
1311
|
const data = body.data;
|
|
1110
1312
|
if (!res.ok || !body.success || !data?.accessToken) {
|
|
1111
1313
|
const err = body.error;
|
|
@@ -1124,14 +1326,18 @@ var SessionManager = class {
|
|
|
1124
1326
|
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: true });
|
|
1125
1327
|
return true;
|
|
1126
1328
|
} catch (err) {
|
|
1127
|
-
this.
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1329
|
+
const aborted = err?.name === "AbortError" || abort.signal.aborted || this.signoutInProgress;
|
|
1330
|
+
if (!aborted) {
|
|
1331
|
+
this.setError({
|
|
1332
|
+
code: "NETWORK_ERROR",
|
|
1333
|
+
message: err instanceof Error ? err.message : "Refresh request failed"
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1131
1336
|
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
1132
1337
|
return false;
|
|
1133
1338
|
} finally {
|
|
1134
1339
|
this.foreignClaim = null;
|
|
1340
|
+
if (this.refreshAbort === abort) this.refreshAbort = null;
|
|
1135
1341
|
}
|
|
1136
1342
|
}
|
|
1137
1343
|
claimWins(foreign, mine) {
|
|
@@ -1158,10 +1364,27 @@ var SessionManager = class {
|
|
|
1158
1364
|
* session and notify subscribers and other tabs.
|
|
1159
1365
|
*/
|
|
1160
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) {
|
|
1161
1384
|
const claims = decodeClaims(accessToken);
|
|
1162
1385
|
const user = claimsToSessionUser(claims);
|
|
1163
|
-
if (refreshToken) {
|
|
1164
|
-
void Promise.resolve(this.tokenStore.write(refreshToken, { claims }));
|
|
1386
|
+
if (opts?.refreshToken) {
|
|
1387
|
+
void Promise.resolve(this.tokenStore.write(opts.refreshToken, { claims }));
|
|
1165
1388
|
}
|
|
1166
1389
|
this.update({
|
|
1167
1390
|
status: user ? "authenticated" : "unauthenticated",
|
|
@@ -1239,6 +1462,14 @@ var SessionManager = class {
|
|
|
1239
1462
|
* the server-side logout request.
|
|
1240
1463
|
*/
|
|
1241
1464
|
signOutLocal(status = "unauthenticated") {
|
|
1465
|
+
this.signoutInProgress = true;
|
|
1466
|
+
if (this.refreshAbort) {
|
|
1467
|
+
try {
|
|
1468
|
+
this.refreshAbort.abort();
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1471
|
+
this.refreshAbort = null;
|
|
1472
|
+
}
|
|
1242
1473
|
void Promise.resolve(this.tokenStore.clear());
|
|
1243
1474
|
if (this.proactiveTimer) {
|
|
1244
1475
|
clearTimeout(this.proactiveTimer);
|
|
@@ -1253,7 +1484,12 @@ var SessionManager = class {
|
|
|
1253
1484
|
error: null,
|
|
1254
1485
|
version: this.snapshot.version + 1
|
|
1255
1486
|
});
|
|
1487
|
+
this.broadcastEnvelope({ type: "refresh:abort", source: this.tabId, ts: Date.now() });
|
|
1256
1488
|
this.broadcast("session:signout");
|
|
1489
|
+
this.idempotencyToken = null;
|
|
1490
|
+
setTimeout(() => {
|
|
1491
|
+
this.signoutInProgress = false;
|
|
1492
|
+
}, 0);
|
|
1257
1493
|
}
|
|
1258
1494
|
/**
|
|
1259
1495
|
* Replace the refresh-token store at runtime. Used by the F22
|
|
@@ -1317,6 +1553,12 @@ var SessionManager = class {
|
|
|
1317
1553
|
}
|
|
1318
1554
|
onBroadcast(env) {
|
|
1319
1555
|
if (!env || env.source === this.tabId) return;
|
|
1556
|
+
if (env.type === "session:probe") {
|
|
1557
|
+
if (this.snapshot.status === "authenticated") {
|
|
1558
|
+
this.broadcast("session:update");
|
|
1559
|
+
}
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1320
1562
|
if (env.type === "refresh:claim") {
|
|
1321
1563
|
this.foreignClaim = { source: env.source, ts: env.ts };
|
|
1322
1564
|
return;
|
|
@@ -1329,6 +1571,24 @@ var SessionManager = class {
|
|
|
1329
1571
|
this.foreignClaim = null;
|
|
1330
1572
|
return;
|
|
1331
1573
|
}
|
|
1574
|
+
if (env.type === "refresh:abort") {
|
|
1575
|
+
this.signoutInProgress = true;
|
|
1576
|
+
if (this.refreshAbort) {
|
|
1577
|
+
try {
|
|
1578
|
+
this.refreshAbort.abort();
|
|
1579
|
+
} catch {
|
|
1580
|
+
}
|
|
1581
|
+
this.refreshAbort = null;
|
|
1582
|
+
}
|
|
1583
|
+
const waiters = this.remoteRefreshWaiters;
|
|
1584
|
+
this.remoteRefreshWaiters = [];
|
|
1585
|
+
for (const w of waiters) w(false);
|
|
1586
|
+
this.foreignClaim = null;
|
|
1587
|
+
setTimeout(() => {
|
|
1588
|
+
this.signoutInProgress = false;
|
|
1589
|
+
}, 0);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1332
1592
|
if (env.type === "session:signout") {
|
|
1333
1593
|
this.update({
|
|
1334
1594
|
status: "unauthenticated",
|
|
@@ -1342,6 +1602,12 @@ var SessionManager = class {
|
|
|
1342
1602
|
return;
|
|
1343
1603
|
}
|
|
1344
1604
|
if ((env.type === "session:update" || env.type === "session:refresh") && env.payload) {
|
|
1605
|
+
if (this.probeResolver && env.payload.status === "authenticated") {
|
|
1606
|
+
const r = this.probeResolver;
|
|
1607
|
+
this.probeResolver = null;
|
|
1608
|
+
r(env.payload);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1345
1611
|
this.update({
|
|
1346
1612
|
...env.payload,
|
|
1347
1613
|
version: Math.max(this.snapshot.version, env.payload.version) + 1
|
|
@@ -1355,6 +1621,32 @@ var SessionManager = class {
|
|
|
1355
1621
|
}
|
|
1356
1622
|
};
|
|
1357
1623
|
|
|
1624
|
+
// src/browser/hostedIssuerGuard.ts
|
|
1625
|
+
var HOSTED_ISSUER_MISMATCH_CODE = "IQAUTH_HOSTED_ISSUER_MISMATCH";
|
|
1626
|
+
var HOSTED_ISSUER_MISMATCH_DOCS_URL = "https://docs.dispositioniq.com/iqauth/errors#hosted-issuer-mismatch";
|
|
1627
|
+
function computeHostedIssuerMismatch(input) {
|
|
1628
|
+
const {
|
|
1629
|
+
nodeEnv,
|
|
1630
|
+
fetchError,
|
|
1631
|
+
explicitOverride,
|
|
1632
|
+
resolvedBaseUrl,
|
|
1633
|
+
managerIssuerUrl,
|
|
1634
|
+
hostedIssuerUrl,
|
|
1635
|
+
appKey
|
|
1636
|
+
} = input;
|
|
1637
|
+
if (nodeEnv === "production") return null;
|
|
1638
|
+
if (!fetchError) return null;
|
|
1639
|
+
if (explicitOverride) return null;
|
|
1640
|
+
if (!managerIssuerUrl || !hostedIssuerUrl) return null;
|
|
1641
|
+
if (resolvedBaseUrl !== managerIssuerUrl) return null;
|
|
1642
|
+
if (resolvedBaseUrl === hostedIssuerUrl) return null;
|
|
1643
|
+
const e = new Error(
|
|
1644
|
+
`[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}`
|
|
1645
|
+
);
|
|
1646
|
+
e.code = HOSTED_ISSUER_MISMATCH_CODE;
|
|
1647
|
+
return e;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1358
1650
|
// src/react/index.tsx
|
|
1359
1651
|
init_signIn();
|
|
1360
1652
|
|
|
@@ -1540,6 +1832,7 @@ var enUS = {
|
|
|
1540
1832
|
"signIn.useDifferentAccount": "Use a different account",
|
|
1541
1833
|
"signIn.selectTenant": "Choose an organization",
|
|
1542
1834
|
"signIn.selectTenantSubtitle": "You belong to multiple organizations. Pick one to continue.",
|
|
1835
|
+
"signIn.selectScope": "Choose scope",
|
|
1543
1836
|
"signIn.dividerOr": "or",
|
|
1544
1837
|
"signIn.preparingExperience": "Preparing your sign-in experience.",
|
|
1545
1838
|
"signIn.applicationUnavailable": "Application unavailable",
|
|
@@ -1628,6 +1921,11 @@ var enUS = {
|
|
|
1628
1921
|
"orgSwitcher.createNew": "Create organization",
|
|
1629
1922
|
"orgSwitcher.manage": "Manage organization",
|
|
1630
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",
|
|
1631
1929
|
"orgProfile.title": "Organization settings",
|
|
1632
1930
|
"orgProfile.generalTab": "General",
|
|
1633
1931
|
"orgProfile.membersTab": "Members",
|
|
@@ -1738,6 +2036,7 @@ function sanitizeReturnTo(input, options = {}) {
|
|
|
1738
2036
|
if (!input || typeof input !== "string") return fallback;
|
|
1739
2037
|
const trimmed = input.trim();
|
|
1740
2038
|
if (!trimmed) return fallback;
|
|
2039
|
+
if (trimmed.includes("\\")) return fallback;
|
|
1741
2040
|
if (trimmed.startsWith("//")) return fallback;
|
|
1742
2041
|
if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
|
|
1743
2042
|
return trimmed;
|
|
@@ -1909,6 +2208,9 @@ function IQAuthProvider({
|
|
|
1909
2208
|
);
|
|
1910
2209
|
return (0, import_react.createElement)(IQAuthContext.Provider, { value }, children);
|
|
1911
2210
|
}
|
|
2211
|
+
function __useIQAuthInternal() {
|
|
2212
|
+
return useCtx();
|
|
2213
|
+
}
|
|
1912
2214
|
function useCtx() {
|
|
1913
2215
|
const ctx = (0, import_react.useContext)(IQAuthContext);
|
|
1914
2216
|
if (!ctx) throw new Error("IQAuth hooks must be used inside <IQAuthProvider>");
|
|
@@ -2220,6 +2522,65 @@ function RedirectToSignIn(props = {}) {
|
|
|
2220
2522
|
}
|
|
2221
2523
|
return null;
|
|
2222
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
|
+
}
|
|
2223
2584
|
function asArray(v) {
|
|
2224
2585
|
if (v == null) return [];
|
|
2225
2586
|
return Array.isArray(v) ? v : [v];
|
|
@@ -2246,7 +2607,14 @@ function claimPermissions(c) {
|
|
|
2246
2607
|
}
|
|
2247
2608
|
return Array.from(out);
|
|
2248
2609
|
}
|
|
2249
|
-
function
|
|
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 }) {
|
|
2250
2618
|
const { snapshot } = useCtx();
|
|
2251
2619
|
if (snapshot.status !== "authenticated") return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2252
2620
|
const wantedRoles = asArray(role);
|
|
@@ -2259,6 +2627,9 @@ function Protect({ role, permission, condition, fallback = null, children }) {
|
|
|
2259
2627
|
const have = new Set(claimPermissions(snapshot.claims));
|
|
2260
2628
|
if (!wantedPerms.some((p) => have.has(p))) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2261
2629
|
}
|
|
2630
|
+
if (scope) {
|
|
2631
|
+
if (!claimSatisfiesScope(snapshot.claims, scope)) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2632
|
+
}
|
|
2262
2633
|
if (condition && !condition(snapshot.claims)) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2263
2634
|
return (0, import_react.createElement)(import_react.Fragment, null, children);
|
|
2264
2635
|
}
|
|
@@ -2815,12 +3186,54 @@ function isSilentSsoEligible(ctx, effectivePrompt) {
|
|
|
2815
3186
|
if (!ctx.returnAllowed) return false;
|
|
2816
3187
|
return true;
|
|
2817
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
|
+
}
|
|
2818
3204
|
function SignIn(props) {
|
|
2819
3205
|
const providerCtx = (0, import_react.useContext)(IQAuthContext);
|
|
2820
|
-
const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.
|
|
3206
|
+
const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.hostedIssuerUrl ?? "";
|
|
2821
3207
|
const appKey = props.appKey ?? providerCtx?.manager.appKey ?? "";
|
|
2822
3208
|
const returnTo = props.returnTo ?? (typeof window !== "undefined" ? `${window.location.origin}/api/iqauth/callback` : "");
|
|
2823
|
-
const
|
|
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 } : {};
|
|
2824
3237
|
const silentSsoEnabled = instanceSilentSso ?? providerCtx?.silentSso ?? false;
|
|
2825
3238
|
const appearance = instanceAppearance && providerCtx?.appearance ? { elements: { ...providerCtx.appearance.elements, ...instanceAppearance.elements } } : instanceAppearance ?? providerCtx?.appearance ?? null;
|
|
2826
3239
|
if (!iqAuthBaseUrl || !appKey) {
|
|
@@ -2831,6 +3244,21 @@ function SignIn(props) {
|
|
|
2831
3244
|
const t2 = useT();
|
|
2832
3245
|
const localeBundle = useLocale();
|
|
2833
3246
|
const { ctx, loading, error } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo);
|
|
3247
|
+
const guardError = computeHostedIssuerMismatch({
|
|
3248
|
+
nodeEnv: typeof process !== "undefined" ? process.env?.NODE_ENV : void 0,
|
|
3249
|
+
fetchError: error,
|
|
3250
|
+
explicitOverride: !!props.iqAuthBaseUrl,
|
|
3251
|
+
resolvedBaseUrl: iqAuthBaseUrl,
|
|
3252
|
+
managerIssuerUrl: providerCtx?.manager.issuerUrl,
|
|
3253
|
+
hostedIssuerUrl: providerCtx?.manager.hostedIssuerUrl,
|
|
3254
|
+
appKey
|
|
3255
|
+
});
|
|
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]);
|
|
2834
3262
|
const preflightLoggedRef = (0, import_react.useRef)(false);
|
|
2835
3263
|
(0, import_react.useEffect)(() => {
|
|
2836
3264
|
if (!ctx || preflightLoggedRef.current) return;
|
|
@@ -2846,6 +3274,7 @@ function SignIn(props) {
|
|
|
2846
3274
|
const [formError, setFormError] = (0, import_react.useState)("");
|
|
2847
3275
|
const [mfa, setMfa] = (0, import_react.useState)(null);
|
|
2848
3276
|
const [tenantSel, setTenantSel] = (0, import_react.useState)(null);
|
|
3277
|
+
const [scopeSel, setScopeSel] = (0, import_react.useState)(null);
|
|
2849
3278
|
const [oauthExchanging, setOauthExchanging] = (0, import_react.useState)(false);
|
|
2850
3279
|
const [silent, setSilent] = (0, import_react.useState)("idle");
|
|
2851
3280
|
const [forcePrompt, setForcePrompt] = (0, import_react.useState)(false);
|
|
@@ -2876,6 +3305,12 @@ function SignIn(props) {
|
|
|
2876
3305
|
setTenantSel({ token: payload.tenantSelectionToken, tenants: payload.tenants || [] });
|
|
2877
3306
|
return true;
|
|
2878
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
|
+
}
|
|
2879
3314
|
if (payload.type === "mfa_required") {
|
|
2880
3315
|
const methods = payload.availableMethods || ["totp"];
|
|
2881
3316
|
setMfa({ token: payload.mfaChallengeToken, methods, selected: methods[0], code: "", backup: false });
|
|
@@ -2896,7 +3331,7 @@ function SignIn(props) {
|
|
|
2896
3331
|
method: "POST",
|
|
2897
3332
|
headers: { "Content-Type": "application/json" },
|
|
2898
3333
|
credentials: "include",
|
|
2899
|
-
body: JSON.stringify({ email, password, ...oidcPayload() })
|
|
3334
|
+
body: JSON.stringify({ email, password, ...oidcPayload(), ...scopeHintBody })
|
|
2900
3335
|
});
|
|
2901
3336
|
const payload = await r.json().catch(() => ({}));
|
|
2902
3337
|
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
@@ -2919,7 +3354,8 @@ function SignIn(props) {
|
|
|
2919
3354
|
code: mfa.code,
|
|
2920
3355
|
method: mfa.selected,
|
|
2921
3356
|
useBackup: mfa.backup,
|
|
2922
|
-
...oidcPayload()
|
|
3357
|
+
...oidcPayload(),
|
|
3358
|
+
...scopeHintBody
|
|
2923
3359
|
})
|
|
2924
3360
|
});
|
|
2925
3361
|
const payload = await r.json().catch(() => ({}));
|
|
@@ -2934,7 +3370,21 @@ function SignIn(props) {
|
|
|
2934
3370
|
method: "POST",
|
|
2935
3371
|
headers: { "Content-Type": "application/json" },
|
|
2936
3372
|
credentials: "include",
|
|
2937
|
-
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 })
|
|
2938
3388
|
});
|
|
2939
3389
|
const payload = await r.json().catch(() => ({}));
|
|
2940
3390
|
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
@@ -3002,6 +3452,11 @@ function SignIn(props) {
|
|
|
3002
3452
|
setSilent("failed");
|
|
3003
3453
|
return;
|
|
3004
3454
|
}
|
|
3455
|
+
if (payload?.type === "scope_selection") {
|
|
3456
|
+
setScopeSel({ token: payload.scopeSelectionToken, tenantId: payload.tenantId, scopes: payload.scopes || [] });
|
|
3457
|
+
setSilent("failed");
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3005
3460
|
setSilent("failed");
|
|
3006
3461
|
} catch {
|
|
3007
3462
|
setSilent("failed");
|
|
@@ -3013,6 +3468,7 @@ function SignIn(props) {
|
|
|
3013
3468
|
setForcePrompt(true);
|
|
3014
3469
|
setSilent("skipped");
|
|
3015
3470
|
setTenantSel(null);
|
|
3471
|
+
setScopeSel(null);
|
|
3016
3472
|
if (typeof window !== "undefined") {
|
|
3017
3473
|
try {
|
|
3018
3474
|
const u = new URL(window.location.href);
|
|
@@ -3083,7 +3539,27 @@ function SignIn(props) {
|
|
|
3083
3539
|
]
|
|
3084
3540
|
},
|
|
3085
3541
|
tn.tenantId
|
|
3086
|
-
)) }) :
|
|
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: [
|
|
3087
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,
|
|
3088
3564
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: mfa.backup ? t2("mfa.backupCodeLabel") : t2("mfa.totpLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3089
3565
|
"input",
|
|
@@ -3375,9 +3851,12 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
|
|
|
3375
3851
|
const t2 = useT();
|
|
3376
3852
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
3377
3853
|
const accent = branding?.accentColor || "#6366f1";
|
|
3854
|
+
const { manager } = useCtx();
|
|
3378
3855
|
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
3379
3856
|
const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
|
|
3380
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);
|
|
3381
3860
|
(0, import_react.useEffect)(() => {
|
|
3382
3861
|
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
3383
3862
|
if (p?.data?.tenantId) setActiveTenantId(p.data.tenantId);
|
|
@@ -3387,18 +3866,48 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
|
|
|
3387
3866
|
});
|
|
3388
3867
|
}, [iqAuthBaseUrl]);
|
|
3389
3868
|
const switchTo = async (tenantId) => {
|
|
3869
|
+
setSwitchError(null);
|
|
3870
|
+
setPendingStep(null);
|
|
3390
3871
|
try {
|
|
3391
|
-
await
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
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
|
+
}
|
|
3396
3890
|
setActiveTenantId(tenantId);
|
|
3397
3891
|
setOpen(false);
|
|
3398
3892
|
onSwitched?.(tenantId);
|
|
3399
|
-
} catch {
|
|
3893
|
+
} catch (err) {
|
|
3894
|
+
setSwitchError(err instanceof Error ? err.message : "Failed to switch organization");
|
|
3400
3895
|
}
|
|
3401
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
|
+
};
|
|
3402
3911
|
const active = memberships.find((m) => m.tenantId === activeTenantId);
|
|
3403
3912
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3404
3913
|
"div",
|
|
@@ -3419,44 +3928,274 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
|
|
|
3419
3928
|
children: active?.tenantName || active?.tenantSlug || t2("orgSwitcher.label")
|
|
3420
3929
|
}
|
|
3421
3930
|
),
|
|
3422
|
-
open ? /* @__PURE__ */ (0, import_jsx_runtime.
|
|
3931
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "menu", style: {
|
|
3423
3932
|
position: "absolute",
|
|
3424
3933
|
left: 0,
|
|
3425
3934
|
top: 36,
|
|
3426
|
-
minWidth:
|
|
3935
|
+
minWidth: 260,
|
|
3427
3936
|
background: "#fff",
|
|
3428
3937
|
border: "1px solid rgba(15,23,42,0.12)",
|
|
3429
3938
|
borderRadius: 8,
|
|
3430
3939
|
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
3431
3940
|
padding: 8,
|
|
3432
3941
|
zIndex: 100
|
|
3433
|
-
}, children:
|
|
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)(
|
|
3434
4159
|
"button",
|
|
3435
4160
|
{
|
|
3436
|
-
|
|
4161
|
+
key,
|
|
3437
4162
|
type: "button",
|
|
3438
|
-
|
|
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
|
+
},
|
|
3439
4178
|
style: {
|
|
3440
4179
|
display: "block",
|
|
3441
4180
|
width: "100%",
|
|
3442
4181
|
textAlign: "left",
|
|
3443
4182
|
padding: "8px 10px",
|
|
3444
|
-
background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
|
|
3445
4183
|
border: "none",
|
|
4184
|
+
background: isActive ? "#f3f4f6" : "transparent",
|
|
4185
|
+
cursor: busy ? "wait" : "pointer",
|
|
3446
4186
|
borderRadius: 4,
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
color: "#0f172a"
|
|
3450
|
-
},
|
|
3451
|
-
children: [
|
|
3452
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
3453
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
|
|
3454
|
-
]
|
|
4187
|
+
fontSize: 13
|
|
4188
|
+
}
|
|
3455
4189
|
},
|
|
3456
|
-
m.
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
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
|
|
3460
4199
|
);
|
|
3461
4200
|
}
|
|
3462
4201
|
function useImpersonation() {
|
|
@@ -4069,10 +4808,13 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
|
|
|
4069
4808
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
4070
4809
|
const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
|
|
4071
4810
|
const accent = branding?.accentColor || "#6366f1";
|
|
4811
|
+
const t2 = useT();
|
|
4812
|
+
const { manager } = useCtx();
|
|
4072
4813
|
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
4073
4814
|
const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
|
|
4074
4815
|
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
4075
4816
|
const [error, setError] = (0, import_react.useState)(null);
|
|
4817
|
+
const [pendingStep, setPendingStep] = (0, import_react.useState)(null);
|
|
4076
4818
|
(0, import_react.useEffect)(() => {
|
|
4077
4819
|
let cancelled = false;
|
|
4078
4820
|
Promise.all([
|
|
@@ -4096,21 +4838,81 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
|
|
|
4096
4838
|
onSelect?.(tenantId);
|
|
4097
4839
|
return;
|
|
4098
4840
|
}
|
|
4841
|
+
setError(null);
|
|
4842
|
+
setPendingStep(null);
|
|
4099
4843
|
try {
|
|
4100
|
-
await
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
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
|
+
}
|
|
4106
4862
|
setActiveTenantId(tenantId);
|
|
4107
4863
|
onSelect?.(tenantId);
|
|
4108
4864
|
} catch (err) {
|
|
4109
4865
|
setError(err instanceof Error ? err.message : "Failed to switch organization");
|
|
4110
4866
|
}
|
|
4111
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
|
+
};
|
|
4112
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: [
|
|
4113
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,
|
|
4114
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) => {
|
|
4115
4917
|
const active = m.tenantId === activeTenantId;
|
|
4116
4918
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -4148,8 +4950,8 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
|
|
|
4148
4950
|
iqAuthBaseUrl,
|
|
4149
4951
|
unstyled: true,
|
|
4150
4952
|
appearance,
|
|
4151
|
-
onCreated: (
|
|
4152
|
-
onSelect?.(
|
|
4953
|
+
onCreated: (t3) => {
|
|
4954
|
+
onSelect?.(t3.id);
|
|
4153
4955
|
reloadList();
|
|
4154
4956
|
},
|
|
4155
4957
|
redirectUrl: createRedirectUrl
|
|
@@ -4402,6 +5204,7 @@ var __version__ = "phase-bc-1.0.0";
|
|
|
4402
5204
|
Protect,
|
|
4403
5205
|
RedirectToSignIn,
|
|
4404
5206
|
RedirectToSignedIn,
|
|
5207
|
+
ScopeSwitcher,
|
|
4405
5208
|
SignIn,
|
|
4406
5209
|
SignUp,
|
|
4407
5210
|
SignedIn,
|
|
@@ -4409,10 +5212,15 @@ var __version__ = "phase-bc-1.0.0";
|
|
|
4409
5212
|
UserButton,
|
|
4410
5213
|
UserProfile,
|
|
4411
5214
|
Waitlist,
|
|
5215
|
+
__useIQAuthInternal,
|
|
4412
5216
|
__version__,
|
|
5217
|
+
claimSatisfiesScope,
|
|
4413
5218
|
isReturnToAllowed,
|
|
4414
5219
|
isSilentSsoEligible,
|
|
5220
|
+
performScopeSwitch,
|
|
5221
|
+
performTenantSwitch,
|
|
4415
5222
|
preflightReturnTo,
|
|
5223
|
+
resolveAfterSignInDestination,
|
|
4416
5224
|
revokeSession,
|
|
4417
5225
|
sanitizeBrandCss,
|
|
4418
5226
|
sanitizeReturnTo,
|
|
@@ -4426,6 +5234,7 @@ var __version__ = "phase-bc-1.0.0";
|
|
|
4426
5234
|
useLinkedIdentities,
|
|
4427
5235
|
useLocale,
|
|
4428
5236
|
useMagicLink,
|
|
5237
|
+
useMemberships,
|
|
4429
5238
|
useOrganization,
|
|
4430
5239
|
usePasskey,
|
|
4431
5240
|
useResolvedSdkBranding,
|