@iqauth/sdk 2.6.4 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +181 -41
- 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 +271 -32
- package/dist/browser.mjs +5 -5
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- 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-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
- package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
- package/dist/chunk-PMAFENVI.mjs +229 -0
- package/dist/chunk-RR2MGPTK.mjs +2724 -0
- package/dist/{chunk-XAWYUPMO.mjs → chunk-RTJAIBXY.mjs} +220 -20
- package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
- package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
- package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
- package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
- package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
- 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-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
- package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
- package/dist/express.d.mts +7 -6
- package/dist/express.d.ts +7 -6
- package/dist/express.js +349 -52
- package/dist/express.mjs +39 -12
- package/dist/fastify.d.mts +2 -0
- package/dist/fastify.d.ts +2 -0
- package/dist/fastify.js +332 -52
- package/dist/fastify.mjs +23 -8
- package/dist/hono.d.mts +2 -0
- package/dist/hono.d.ts +2 -0
- package/dist/hono.js +329 -52
- package/dist/hono.mjs +20 -8
- package/dist/index-5KSZEnDe.d.ts +1626 -0
- package/dist/index-CKoZHAoc.d.mts +1626 -0
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +565 -69
- package/dist/index.mjs +29 -9
- 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/mobile.d.mts +77 -7
- package/dist/mobile.d.ts +77 -7
- package/dist/mobile.js +276 -41
- package/dist/mobile.mjs +98 -3
- package/dist/next.d.mts +2 -1
- package/dist/next.d.ts +2 -1
- package/dist/next.js +391 -201
- package/dist/next.mjs +22 -7
- package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
- package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
- 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 +97 -0
- package/dist/react.d.mts +9 -1624
- package/dist/react.d.ts +9 -1624
- package/dist/react.js +313 -33
- package/dist/react.mjs +58 -2632
- package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
- package/dist/server/handlers.d.mts +148 -3
- package/dist/server/handlers.d.ts +148 -3
- package/dist/server/handlers.js +410 -11
- package/dist/server/handlers.mjs +12 -3
- package/dist/server.d.mts +151 -8
- package/dist/server.d.ts +151 -8
- package/dist/server.js +406 -50
- package/dist/server.mjs +93 -11
- package/dist/service.d.mts +4 -4
- package/dist/service.d.ts +4 -4
- package/dist/service.js +181 -41
- package/dist/service.mjs +3 -3
- package/dist/{signIn-OCr88Zf8.d.ts → signIn-BLFnz8SV.d.ts} +78 -3
- package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
- package/dist/{signIn-CiIBTJIh.d.mts → signIn-T-CZ6t6r.d.mts} +78 -3
- package/dist/test.mjs +3 -3
- package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
- package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
- package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
- package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
- package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
- package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
- package/dist/webhooks.d.mts +100 -17
- package/dist/webhooks.d.ts +100 -17
- package/dist/webhooks.js +164 -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/package.json +13 -3
- 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
|
}
|
|
@@ -789,6 +828,7 @@ __export(react_exports, {
|
|
|
789
828
|
UserButton: () => UserButton,
|
|
790
829
|
UserProfile: () => UserProfile,
|
|
791
830
|
Waitlist: () => Waitlist,
|
|
831
|
+
__useIQAuthInternal: () => __useIQAuthInternal,
|
|
792
832
|
__version__: () => __version__,
|
|
793
833
|
isReturnToAllowed: () => isReturnToAllowed,
|
|
794
834
|
isSilentSsoEligible: () => isSilentSsoEligible,
|
|
@@ -854,14 +894,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
854
894
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
855
895
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
856
896
|
throw new IQAuthError(
|
|
857
|
-
"
|
|
897
|
+
"config_invalid",
|
|
858
898
|
`${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
899
|
);
|
|
860
900
|
}
|
|
861
901
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
862
902
|
if (!shapeMatch) {
|
|
863
903
|
throw new IQAuthError(
|
|
864
|
-
"
|
|
904
|
+
"config_invalid",
|
|
865
905
|
`${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
906
|
);
|
|
867
907
|
}
|
|
@@ -870,19 +910,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
870
910
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
871
911
|
} catch {
|
|
872
912
|
throw new IQAuthError(
|
|
873
|
-
"
|
|
913
|
+
"config_invalid",
|
|
874
914
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
875
915
|
);
|
|
876
916
|
}
|
|
877
917
|
if (!isPublishableKeyPayload(decoded)) {
|
|
878
918
|
throw new IQAuthError(
|
|
879
|
-
"
|
|
919
|
+
"config_invalid",
|
|
880
920
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
881
921
|
);
|
|
882
922
|
}
|
|
883
923
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
884
924
|
throw new IQAuthError(
|
|
885
|
-
"
|
|
925
|
+
"config_invalid",
|
|
886
926
|
`${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
927
|
);
|
|
888
928
|
}
|
|
@@ -896,6 +936,7 @@ function isPublishableKeyPayload(value) {
|
|
|
896
936
|
|
|
897
937
|
// src/browser/sessionManager.ts
|
|
898
938
|
init_storage();
|
|
939
|
+
var PROBE_WAIT_MS = 80;
|
|
899
940
|
var DEFAULT_REFRESH_PATH = "/api/v1/auth/refresh";
|
|
900
941
|
var DEFAULT_USERINFO_PATH = "/api/v1/auth/me";
|
|
901
942
|
async function readAuthErrorCode(res) {
|
|
@@ -933,7 +974,13 @@ function claimsToSessionUser(claims) {
|
|
|
933
974
|
tenantId: claims.tenantId,
|
|
934
975
|
vendorId: claims.vendorId,
|
|
935
976
|
roles: claims.roles ?? [],
|
|
936
|
-
entitlements: claims.entitlements ?? []
|
|
977
|
+
entitlements: claims.entitlements ?? [],
|
|
978
|
+
// SDK 2.7.0 (Task #124) — pass through identity claims when issued.
|
|
979
|
+
...claims.picture !== void 0 ? { picture: claims.picture } : {},
|
|
980
|
+
...claims.email_verified !== void 0 ? { emailVerified: claims.email_verified } : {},
|
|
981
|
+
...claims.given_name !== void 0 ? { givenName: claims.given_name } : {},
|
|
982
|
+
...claims.family_name !== void 0 ? { familyName: claims.family_name } : {},
|
|
983
|
+
...claims.locale !== void 0 ? { locale: claims.locale } : {}
|
|
937
984
|
};
|
|
938
985
|
}
|
|
939
986
|
var EMPTY = {
|
|
@@ -957,11 +1004,50 @@ var NO_OP_STORE = {
|
|
|
957
1004
|
write: () => void 0,
|
|
958
1005
|
clear: () => void 0
|
|
959
1006
|
};
|
|
1007
|
+
var IDEMPOTENCY_HEADER = "X-IQAuth-Idempotency";
|
|
1008
|
+
function randomIdempotencyToken() {
|
|
1009
|
+
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
1010
|
+
const bytes = new Uint8Array(16);
|
|
1011
|
+
crypto.getRandomValues(bytes);
|
|
1012
|
+
let out = "";
|
|
1013
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1014
|
+
return out;
|
|
1015
|
+
}
|
|
1016
|
+
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
1017
|
+
}
|
|
960
1018
|
var SessionManager = class {
|
|
961
1019
|
constructor(options) {
|
|
962
1020
|
this.snapshot = { ...EMPTY };
|
|
963
1021
|
this.listeners = /* @__PURE__ */ new Set();
|
|
964
1022
|
this.refreshPromise = null;
|
|
1023
|
+
/**
|
|
1024
|
+
* Cancellation handle for the in-flight refresh, if any. `signOut()` (or a
|
|
1025
|
+
* `session:signout` broadcast from another tab) calls `abort()` so the
|
|
1026
|
+
* refresh response is dropped before it can write a fresh access cookie
|
|
1027
|
+
* on top of the just-cleared session — the second root cause of "ghost
|
|
1028
|
+
* signed-in" sessions after Sign Out.
|
|
1029
|
+
*/
|
|
1030
|
+
this.refreshAbort = null;
|
|
1031
|
+
/**
|
|
1032
|
+
* Set to `true` by `signOut()` / `signOutLocal()` for the lifetime of the
|
|
1033
|
+
* call. Used as a safety belt: even if a refresh response arrives while
|
|
1034
|
+
* `refreshAbort` was unable to interrupt the network call (e.g. the body
|
|
1035
|
+
* was already streaming back), `runRefresh` checks this flag before
|
|
1036
|
+
* mutating session state and bails out.
|
|
1037
|
+
*/
|
|
1038
|
+
this.signoutInProgress = false;
|
|
1039
|
+
/**
|
|
1040
|
+
* Per-session opaque idempotency token. Sent as `X-IQAuth-Idempotency` on
|
|
1041
|
+
* every /refresh and /signout request the SDK makes through a framework
|
|
1042
|
+
* adapter (Express/Fastify/Hono/Next), so the adapter's `SignoutRegistry`
|
|
1043
|
+
* can collapse a refresh that lands moments after a signout — even when
|
|
1044
|
+
* the two requests are routed to different server instances (multi-replica
|
|
1045
|
+
* deployments).
|
|
1046
|
+
*
|
|
1047
|
+
* Generated lazily on first use, rotated on signout so the next session
|
|
1048
|
+
* starts with a fresh token. Opaque random — never the raw refresh token.
|
|
1049
|
+
*/
|
|
1050
|
+
this.idempotencyToken = null;
|
|
965
1051
|
this.channel = null;
|
|
966
1052
|
this.proactiveTimer = null;
|
|
967
1053
|
this.bootstrapped = false;
|
|
@@ -969,6 +1055,8 @@ var SessionManager = class {
|
|
|
969
1055
|
this.remoteRefreshWaiters = [];
|
|
970
1056
|
/** Active claims by other tabs (keyed by source tabId). */
|
|
971
1057
|
this.foreignClaim = null;
|
|
1058
|
+
/** Resolver for an in-flight cross-tab `session:probe`, set during bootstrap. */
|
|
1059
|
+
this.probeResolver = null;
|
|
972
1060
|
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
|
|
973
1061
|
this.key = parsed;
|
|
974
1062
|
const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
|
|
@@ -981,6 +1069,8 @@ var SessionManager = class {
|
|
|
981
1069
|
this.refreshCookieName = options.cookieNames?.refresh ?? REFRESH_COOKIE;
|
|
982
1070
|
this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore(this.refreshCookieName) : NO_OP_STORE);
|
|
983
1071
|
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
1072
|
+
this.debug = options.debug ?? false;
|
|
1073
|
+
this.onTimingEvent = options.onTimingEvent ?? null;
|
|
984
1074
|
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
985
1075
|
throw new Error("global fetch is not available; pass fetchImpl");
|
|
986
1076
|
}));
|
|
@@ -1007,10 +1097,35 @@ var SessionManager = class {
|
|
|
1007
1097
|
get issuerUrl() {
|
|
1008
1098
|
return this.issuer;
|
|
1009
1099
|
}
|
|
1100
|
+
/**
|
|
1101
|
+
* SDK 2.7.0 (Task #124) — The hosted IQAuth host derived from the
|
|
1102
|
+
* publishable key's `iss` claim, normalized to URL form. This is what
|
|
1103
|
+
* `<SignIn/>` and `buildSignInUrl` use to talk to the hosted UI; it
|
|
1104
|
+
* deliberately ignores the `issuer` constructor override so a misrouted
|
|
1105
|
+
* `issuer` (e.g. pointed at the consumer app's own domain) cannot break
|
|
1106
|
+
* the hosted flow. Use {@link issuerUrl} for token / discovery endpoints.
|
|
1107
|
+
*/
|
|
1108
|
+
get hostedIssuerUrl() {
|
|
1109
|
+
const iss = this.key.iss;
|
|
1110
|
+
return (iss.startsWith("http") ? iss : `https://${iss}`).replace(/\/+$/, "");
|
|
1111
|
+
}
|
|
1010
1112
|
/** Cookie name the SDK uses for the refresh token (overridable via `cookieNames.refresh`). */
|
|
1011
1113
|
get refreshCookie() {
|
|
1012
1114
|
return this.refreshCookieName;
|
|
1013
1115
|
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Returns the current per-session idempotency token, generating one
|
|
1118
|
+
* lazily on first use. Sent as the `X-IQAuth-Idempotency` header on
|
|
1119
|
+
* /refresh and /signout requests so the framework adapter's
|
|
1120
|
+
* `SignoutRegistry` can collapse a refresh-vs-signout race even across
|
|
1121
|
+
* server instances.
|
|
1122
|
+
*/
|
|
1123
|
+
getIdempotencyToken() {
|
|
1124
|
+
if (!this.idempotencyToken) {
|
|
1125
|
+
this.idempotencyToken = randomIdempotencyToken();
|
|
1126
|
+
}
|
|
1127
|
+
return this.idempotencyToken;
|
|
1128
|
+
}
|
|
1014
1129
|
getSnapshot() {
|
|
1015
1130
|
return this.snapshot;
|
|
1016
1131
|
}
|
|
@@ -1024,9 +1139,44 @@ var SessionManager = class {
|
|
|
1024
1139
|
* One-time bootstrap: warm the session from the refresh cookie if present.
|
|
1025
1140
|
* Safe to call multiple times.
|
|
1026
1141
|
*/
|
|
1142
|
+
/**
|
|
1143
|
+
* Task #126: Public timing-event emitter. Used by the browser sign-in
|
|
1144
|
+
* helpers (redirectToSignIn / handleAuthCallback) to surface signIn-phase
|
|
1145
|
+
* timings through the same `debug` + `onTimingEvent` channel as
|
|
1146
|
+
* bootstrap/refresh. Safe to call from anywhere — internal callers
|
|
1147
|
+
* pre-compute durationMs.
|
|
1148
|
+
*/
|
|
1149
|
+
recordTiming(phase, durationMs, ok, code) {
|
|
1150
|
+
this.emitTiming(phase, durationMs, ok, code);
|
|
1151
|
+
}
|
|
1152
|
+
/** Task #126: emit a session timing event to debug log + onTimingEvent hook. */
|
|
1153
|
+
emitTiming(phase, durationMs, ok, code) {
|
|
1154
|
+
if (this.debug) {
|
|
1155
|
+
try {
|
|
1156
|
+
console.debug("[iqauth_session]", { phase, durationMs, ok, code });
|
|
1157
|
+
} catch {
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (this.onTimingEvent) {
|
|
1161
|
+
try {
|
|
1162
|
+
this.onTimingEvent({ phase, durationMs, ok, code });
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1027
1167
|
async bootstrap() {
|
|
1028
1168
|
if (this.bootstrapped) return;
|
|
1029
1169
|
this.bootstrapped = true;
|
|
1170
|
+
const t0 = Date.now();
|
|
1171
|
+
try {
|
|
1172
|
+
await this.bootstrapInner();
|
|
1173
|
+
this.emitTiming("bootstrap", Date.now() - t0, this.snapshot.status === "authenticated");
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
this.emitTiming("bootstrap", Date.now() - t0, false, err instanceof Error ? err.message : "ERROR");
|
|
1176
|
+
throw err;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
async bootstrapInner() {
|
|
1030
1180
|
if (this.serverManagedSession) {
|
|
1031
1181
|
try {
|
|
1032
1182
|
const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
|
|
@@ -1061,6 +1211,15 @@ var SessionManager = class {
|
|
|
1061
1211
|
return;
|
|
1062
1212
|
}
|
|
1063
1213
|
}
|
|
1214
|
+
const peerSnapshot = await this.probePeers();
|
|
1215
|
+
if (peerSnapshot && peerSnapshot.status === "authenticated") {
|
|
1216
|
+
this.update({
|
|
1217
|
+
...peerSnapshot,
|
|
1218
|
+
version: Math.max(this.snapshot.version, peerSnapshot.version) + 1
|
|
1219
|
+
});
|
|
1220
|
+
this.scheduleProactiveRefresh();
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1064
1223
|
const stored = await Promise.resolve(this.tokenStore.read());
|
|
1065
1224
|
if (!stored) {
|
|
1066
1225
|
this.setStatus("unauthenticated");
|
|
@@ -1069,6 +1228,22 @@ var SessionManager = class {
|
|
|
1069
1228
|
const ok = await this.refresh();
|
|
1070
1229
|
if (!ok) this.setStatus("unauthenticated");
|
|
1071
1230
|
}
|
|
1231
|
+
probePeers() {
|
|
1232
|
+
if (!this.channel) return Promise.resolve(null);
|
|
1233
|
+
return new Promise((resolve) => {
|
|
1234
|
+
let settled = false;
|
|
1235
|
+
const finish = (snap) => {
|
|
1236
|
+
if (settled) return;
|
|
1237
|
+
settled = true;
|
|
1238
|
+
this.probeResolver = null;
|
|
1239
|
+
clearTimeout(timer);
|
|
1240
|
+
resolve(snap);
|
|
1241
|
+
};
|
|
1242
|
+
this.probeResolver = (snap) => finish(snap);
|
|
1243
|
+
const timer = setTimeout(() => finish(null), PROBE_WAIT_MS);
|
|
1244
|
+
this.broadcastEnvelope({ type: "session:probe", source: this.tabId, ts: Date.now() });
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1072
1247
|
/**
|
|
1073
1248
|
* Single-flight token refresh, coordinated across tabs via BroadcastChannel.
|
|
1074
1249
|
*
|
|
@@ -1082,30 +1257,48 @@ var SessionManager = class {
|
|
|
1082
1257
|
*/
|
|
1083
1258
|
refresh() {
|
|
1084
1259
|
if (this.refreshPromise) return this.refreshPromise;
|
|
1085
|
-
|
|
1260
|
+
const t0 = Date.now();
|
|
1261
|
+
this.refreshPromise = this.runRefresh().then((ok) => {
|
|
1262
|
+
this.emitTiming("refresh", Date.now() - t0, ok, ok ? void 0 : this.snapshot.error?.code ?? "REFRESH_FAILED");
|
|
1263
|
+
return ok;
|
|
1264
|
+
}).finally(() => {
|
|
1086
1265
|
this.refreshPromise = null;
|
|
1087
1266
|
});
|
|
1088
1267
|
return this.refreshPromise;
|
|
1089
1268
|
}
|
|
1090
1269
|
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
|
-
}
|
|
1270
|
+
const abort = new AbortController();
|
|
1271
|
+
this.refreshAbort = abort;
|
|
1100
1272
|
try {
|
|
1273
|
+
const myClaim = { source: this.tabId, ts: Date.now() };
|
|
1274
|
+
if (this.channel) {
|
|
1275
|
+
this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
|
|
1276
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
1277
|
+
if (abort.signal.aborted || this.signoutInProgress) {
|
|
1278
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
const foreign = this.foreignClaim;
|
|
1282
|
+
if (foreign && this.claimWins(foreign, myClaim)) {
|
|
1283
|
+
return this.waitForForeignRefresh();
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1101
1286
|
const refreshToken = await Promise.resolve(this.tokenStore.read());
|
|
1102
1287
|
const res = await this.fetchImpl(`${this.issuer}${this.refreshPath}`, {
|
|
1103
1288
|
method: "POST",
|
|
1104
1289
|
credentials: "include",
|
|
1105
|
-
headers: {
|
|
1106
|
-
|
|
1290
|
+
headers: {
|
|
1291
|
+
"Content-Type": "application/json",
|
|
1292
|
+
[IDEMPOTENCY_HEADER]: this.getIdempotencyToken()
|
|
1293
|
+
},
|
|
1294
|
+
body: JSON.stringify(refreshToken ? { refreshToken } : {}),
|
|
1295
|
+
signal: abort.signal
|
|
1107
1296
|
});
|
|
1108
1297
|
const body = await res.json().catch(() => ({}));
|
|
1298
|
+
if (this.signoutInProgress || abort.signal.aborted) {
|
|
1299
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1109
1302
|
const data = body.data;
|
|
1110
1303
|
if (!res.ok || !body.success || !data?.accessToken) {
|
|
1111
1304
|
const err = body.error;
|
|
@@ -1124,14 +1317,18 @@ var SessionManager = class {
|
|
|
1124
1317
|
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: true });
|
|
1125
1318
|
return true;
|
|
1126
1319
|
} catch (err) {
|
|
1127
|
-
this.
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1320
|
+
const aborted = err?.name === "AbortError" || abort.signal.aborted || this.signoutInProgress;
|
|
1321
|
+
if (!aborted) {
|
|
1322
|
+
this.setError({
|
|
1323
|
+
code: "NETWORK_ERROR",
|
|
1324
|
+
message: err instanceof Error ? err.message : "Refresh request failed"
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1131
1327
|
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
1132
1328
|
return false;
|
|
1133
1329
|
} finally {
|
|
1134
1330
|
this.foreignClaim = null;
|
|
1331
|
+
if (this.refreshAbort === abort) this.refreshAbort = null;
|
|
1135
1332
|
}
|
|
1136
1333
|
}
|
|
1137
1334
|
claimWins(foreign, mine) {
|
|
@@ -1239,6 +1436,14 @@ var SessionManager = class {
|
|
|
1239
1436
|
* the server-side logout request.
|
|
1240
1437
|
*/
|
|
1241
1438
|
signOutLocal(status = "unauthenticated") {
|
|
1439
|
+
this.signoutInProgress = true;
|
|
1440
|
+
if (this.refreshAbort) {
|
|
1441
|
+
try {
|
|
1442
|
+
this.refreshAbort.abort();
|
|
1443
|
+
} catch {
|
|
1444
|
+
}
|
|
1445
|
+
this.refreshAbort = null;
|
|
1446
|
+
}
|
|
1242
1447
|
void Promise.resolve(this.tokenStore.clear());
|
|
1243
1448
|
if (this.proactiveTimer) {
|
|
1244
1449
|
clearTimeout(this.proactiveTimer);
|
|
@@ -1253,7 +1458,12 @@ var SessionManager = class {
|
|
|
1253
1458
|
error: null,
|
|
1254
1459
|
version: this.snapshot.version + 1
|
|
1255
1460
|
});
|
|
1461
|
+
this.broadcastEnvelope({ type: "refresh:abort", source: this.tabId, ts: Date.now() });
|
|
1256
1462
|
this.broadcast("session:signout");
|
|
1463
|
+
this.idempotencyToken = null;
|
|
1464
|
+
setTimeout(() => {
|
|
1465
|
+
this.signoutInProgress = false;
|
|
1466
|
+
}, 0);
|
|
1257
1467
|
}
|
|
1258
1468
|
/**
|
|
1259
1469
|
* Replace the refresh-token store at runtime. Used by the F22
|
|
@@ -1317,6 +1527,12 @@ var SessionManager = class {
|
|
|
1317
1527
|
}
|
|
1318
1528
|
onBroadcast(env) {
|
|
1319
1529
|
if (!env || env.source === this.tabId) return;
|
|
1530
|
+
if (env.type === "session:probe") {
|
|
1531
|
+
if (this.snapshot.status === "authenticated") {
|
|
1532
|
+
this.broadcast("session:update");
|
|
1533
|
+
}
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1320
1536
|
if (env.type === "refresh:claim") {
|
|
1321
1537
|
this.foreignClaim = { source: env.source, ts: env.ts };
|
|
1322
1538
|
return;
|
|
@@ -1329,6 +1545,24 @@ var SessionManager = class {
|
|
|
1329
1545
|
this.foreignClaim = null;
|
|
1330
1546
|
return;
|
|
1331
1547
|
}
|
|
1548
|
+
if (env.type === "refresh:abort") {
|
|
1549
|
+
this.signoutInProgress = true;
|
|
1550
|
+
if (this.refreshAbort) {
|
|
1551
|
+
try {
|
|
1552
|
+
this.refreshAbort.abort();
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
this.refreshAbort = null;
|
|
1556
|
+
}
|
|
1557
|
+
const waiters = this.remoteRefreshWaiters;
|
|
1558
|
+
this.remoteRefreshWaiters = [];
|
|
1559
|
+
for (const w of waiters) w(false);
|
|
1560
|
+
this.foreignClaim = null;
|
|
1561
|
+
setTimeout(() => {
|
|
1562
|
+
this.signoutInProgress = false;
|
|
1563
|
+
}, 0);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1332
1566
|
if (env.type === "session:signout") {
|
|
1333
1567
|
this.update({
|
|
1334
1568
|
status: "unauthenticated",
|
|
@@ -1342,6 +1576,12 @@ var SessionManager = class {
|
|
|
1342
1576
|
return;
|
|
1343
1577
|
}
|
|
1344
1578
|
if ((env.type === "session:update" || env.type === "session:refresh") && env.payload) {
|
|
1579
|
+
if (this.probeResolver && env.payload.status === "authenticated") {
|
|
1580
|
+
const r = this.probeResolver;
|
|
1581
|
+
this.probeResolver = null;
|
|
1582
|
+
r(env.payload);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1345
1585
|
this.update({
|
|
1346
1586
|
...env.payload,
|
|
1347
1587
|
version: Math.max(this.snapshot.version, env.payload.version) + 1
|
|
@@ -1355,6 +1595,32 @@ var SessionManager = class {
|
|
|
1355
1595
|
}
|
|
1356
1596
|
};
|
|
1357
1597
|
|
|
1598
|
+
// src/browser/hostedIssuerGuard.ts
|
|
1599
|
+
var HOSTED_ISSUER_MISMATCH_CODE = "IQAUTH_HOSTED_ISSUER_MISMATCH";
|
|
1600
|
+
var HOSTED_ISSUER_MISMATCH_DOCS_URL = "https://docs.dispositioniq.com/iqauth/errors#hosted-issuer-mismatch";
|
|
1601
|
+
function computeHostedIssuerMismatch(input) {
|
|
1602
|
+
const {
|
|
1603
|
+
nodeEnv,
|
|
1604
|
+
fetchError,
|
|
1605
|
+
explicitOverride,
|
|
1606
|
+
resolvedBaseUrl,
|
|
1607
|
+
managerIssuerUrl,
|
|
1608
|
+
hostedIssuerUrl,
|
|
1609
|
+
appKey
|
|
1610
|
+
} = input;
|
|
1611
|
+
if (nodeEnv === "production") return null;
|
|
1612
|
+
if (!fetchError) return null;
|
|
1613
|
+
if (explicitOverride) return null;
|
|
1614
|
+
if (!managerIssuerUrl || !hostedIssuerUrl) return null;
|
|
1615
|
+
if (resolvedBaseUrl !== managerIssuerUrl) return null;
|
|
1616
|
+
if (resolvedBaseUrl === hostedIssuerUrl) return null;
|
|
1617
|
+
const e = new Error(
|
|
1618
|
+
`[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}`
|
|
1619
|
+
);
|
|
1620
|
+
e.code = HOSTED_ISSUER_MISMATCH_CODE;
|
|
1621
|
+
return e;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1358
1624
|
// src/react/index.tsx
|
|
1359
1625
|
init_signIn();
|
|
1360
1626
|
|
|
@@ -1909,6 +2175,9 @@ function IQAuthProvider({
|
|
|
1909
2175
|
);
|
|
1910
2176
|
return (0, import_react.createElement)(IQAuthContext.Provider, { value }, children);
|
|
1911
2177
|
}
|
|
2178
|
+
function __useIQAuthInternal() {
|
|
2179
|
+
return useCtx();
|
|
2180
|
+
}
|
|
1912
2181
|
function useCtx() {
|
|
1913
2182
|
const ctx = (0, import_react.useContext)(IQAuthContext);
|
|
1914
2183
|
if (!ctx) throw new Error("IQAuth hooks must be used inside <IQAuthProvider>");
|
|
@@ -2817,7 +3086,7 @@ function isSilentSsoEligible(ctx, effectivePrompt) {
|
|
|
2817
3086
|
}
|
|
2818
3087
|
function SignIn(props) {
|
|
2819
3088
|
const providerCtx = (0, import_react.useContext)(IQAuthContext);
|
|
2820
|
-
const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.
|
|
3089
|
+
const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.hostedIssuerUrl ?? "";
|
|
2821
3090
|
const appKey = props.appKey ?? providerCtx?.manager.appKey ?? "";
|
|
2822
3091
|
const returnTo = props.returnTo ?? (typeof window !== "undefined" ? `${window.location.origin}/api/iqauth/callback` : "");
|
|
2823
3092
|
const { onRedirect, className, prompt, appearance: instanceAppearance, silentSso: instanceSilentSso } = props;
|
|
@@ -2831,6 +3100,16 @@ function SignIn(props) {
|
|
|
2831
3100
|
const t2 = useT();
|
|
2832
3101
|
const localeBundle = useLocale();
|
|
2833
3102
|
const { ctx, loading, error } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo);
|
|
3103
|
+
const guardError = computeHostedIssuerMismatch({
|
|
3104
|
+
nodeEnv: typeof process !== "undefined" ? process.env?.NODE_ENV : void 0,
|
|
3105
|
+
fetchError: error,
|
|
3106
|
+
explicitOverride: !!props.iqAuthBaseUrl,
|
|
3107
|
+
resolvedBaseUrl: iqAuthBaseUrl,
|
|
3108
|
+
managerIssuerUrl: providerCtx?.manager.issuerUrl,
|
|
3109
|
+
hostedIssuerUrl: providerCtx?.manager.hostedIssuerUrl,
|
|
3110
|
+
appKey
|
|
3111
|
+
});
|
|
3112
|
+
if (guardError) throw guardError;
|
|
2834
3113
|
const preflightLoggedRef = (0, import_react.useRef)(false);
|
|
2835
3114
|
(0, import_react.useEffect)(() => {
|
|
2836
3115
|
if (!ctx || preflightLoggedRef.current) return;
|
|
@@ -4409,6 +4688,7 @@ var __version__ = "phase-bc-1.0.0";
|
|
|
4409
4688
|
UserButton,
|
|
4410
4689
|
UserProfile,
|
|
4411
4690
|
Waitlist,
|
|
4691
|
+
__useIQAuthInternal,
|
|
4412
4692
|
__version__,
|
|
4413
4693
|
isReturnToAllowed,
|
|
4414
4694
|
isSilentSsoEligible,
|