@iqauth/sdk 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +24 -0
  2. package/dist/browser-session.d.mts +1 -2
  3. package/dist/browser-session.d.ts +1 -2
  4. package/dist/browser-session.js +89 -68
  5. package/dist/browser-session.mjs +2 -1
  6. package/dist/browser.d.mts +1 -1
  7. package/dist/browser.d.ts +1 -1
  8. package/dist/browser.js +13 -2
  9. package/dist/browser.mjs +2 -2
  10. package/dist/{chunk-D72UL5HL.mjs → chunk-EKTNEZIH.mjs} +4 -4
  11. package/dist/{chunk-M4J6BPK7.mjs → chunk-KGEPDXHU.mjs} +10 -1
  12. package/dist/{chunk-QZB745C2.mjs → chunk-RACIPVLD.mjs} +13 -2
  13. package/dist/chunk-UNYDG2L4.mjs +209 -0
  14. package/dist/{chunk-MDUHPQMM.mjs → chunk-W3F4JYGP.mjs} +8 -180
  15. package/dist/{chunk-QEJB7WEQ.mjs → chunk-WQWBJSSS.mjs} +1 -1
  16. package/dist/cli/index.mjs +1 -1
  17. package/dist/{client-DXbHb2ul.d.ts → client-DTX4hNdS.d.ts} +16 -21
  18. package/dist/{client-Dv4v92Mj.d.mts → client-vdh2a9fJ.d.mts} +16 -21
  19. package/dist/{doctor-XCI77BQS.mjs → doctor-A5E7LSFW.mjs} +1 -1
  20. package/dist/{express-BZmF1llh.d.mts → express-A0-dWEMy.d.mts} +1 -1
  21. package/dist/{express-B4o3P8vK.d.ts → express-Bo_pJKHN.d.ts} +1 -1
  22. package/dist/express.d.mts +75 -5
  23. package/dist/express.d.ts +75 -5
  24. package/dist/express.js +300 -70
  25. package/dist/express.mjs +208 -7
  26. package/dist/fastify.js +101 -70
  27. package/dist/fastify.mjs +8 -6
  28. package/dist/hono.js +100 -70
  29. package/dist/hono.mjs +7 -6
  30. package/dist/index.d.mts +2 -3
  31. package/dist/index.d.ts +2 -3
  32. package/dist/index.js +90 -69
  33. package/dist/index.mjs +15 -13
  34. package/dist/mobile.d.mts +1 -2
  35. package/dist/mobile.d.ts +1 -2
  36. package/dist/mobile.js +89 -68
  37. package/dist/mobile.mjs +2 -1
  38. package/dist/next.d.mts +9 -0
  39. package/dist/next.d.ts +9 -0
  40. package/dist/next.js +99 -1616
  41. package/dist/next.mjs +9 -9
  42. package/dist/react.d.mts +1 -1
  43. package/dist/react.d.ts +1 -1
  44. package/dist/react.js +13 -2
  45. package/dist/react.mjs +2 -2
  46. package/dist/server/handlers.d.mts +2 -0
  47. package/dist/server/handlers.d.ts +2 -0
  48. package/dist/server/handlers.js +10 -1
  49. package/dist/server/handlers.mjs +2 -2
  50. package/dist/server.d.mts +2 -3
  51. package/dist/server.d.ts +2 -3
  52. package/dist/server.js +99 -69
  53. package/dist/server.mjs +7 -6
  54. package/dist/service.d.mts +1 -2
  55. package/dist/service.d.ts +1 -2
  56. package/dist/service.js +89 -68
  57. package/dist/service.mjs +2 -1
  58. package/dist/{signIn-D_kP3v-c.d.mts → signIn-Cd0P4y9d.d.mts} +8 -0
  59. package/dist/{signIn-BVDTIA_t.d.ts → signIn-DKakyzeu.d.ts} +8 -0
  60. package/package.json +3 -2
package/dist/express.js CHANGED
@@ -445,8 +445,7 @@ function parseMfaResponse(data, browserSessionMode) {
445
445
  }
446
446
 
447
447
  // src/modules/tokens.ts
448
- var import_crypto = __toESM(require("crypto"));
449
- var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
448
+ var import_jose = require("jose");
450
449
  var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
451
450
  var DEFAULT_TOKEN_ISSUER = [
452
451
  "https://auth.dispositioniq.com",
@@ -459,6 +458,24 @@ var DEFAULT_TOKEN_AUDIENCE = [
459
458
  "iqvalidate"
460
459
  ];
461
460
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
461
+ function decodeProtectedHeader(token) {
462
+ const parts = token.split(".");
463
+ if (parts.length < 2) return null;
464
+ try {
465
+ const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
466
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
467
+ let json;
468
+ if (typeof atob === "function") {
469
+ json = atob(b64);
470
+ } else {
471
+ const { Buffer: Buffer2 } = require("buffer");
472
+ json = Buffer2.from(b64, "base64").toString("utf8");
473
+ }
474
+ return JSON.parse(json);
475
+ } catch {
476
+ return null;
477
+ }
478
+ }
462
479
  var TokensModule = class {
463
480
  constructor(baseUrl, options = {}) {
464
481
  this.jwksCache = null;
@@ -469,49 +486,49 @@ var TokensModule = class {
469
486
  this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
470
487
  }
471
488
  /**
472
- * Verify a JWT access token using RS256 via JWKS from /.well-known/jwks.json.
473
- * Caches JWKS keys for 1 hour. Retries once on unknown `kid`.
474
- *
475
- * @remarks Validates against /.well-known/jwks.json. Issuer, audience, and
476
- * clock tolerance default to client config but can be overridden per call.
489
+ * Verify a JWT access token using RS256/ES256 via JWKS from
490
+ * `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
491
+ * Node, browser, and edge runtimes alike — no `node:crypto` dependency.
492
+ * Caches JWKS for 1 hour and refetches once on unknown `kid`.
477
493
  */
478
494
  async verify(token, options = {}) {
479
- const decoded = import_jsonwebtoken.default.decode(token, { complete: true });
480
- if (!decoded || typeof decoded === "string") {
495
+ const header = decodeProtectedHeader(token);
496
+ if (!header) {
481
497
  throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
482
498
  }
483
- const kid = decoded.header.kid;
499
+ const kid = header.kid;
484
500
  if (!kid) {
485
501
  throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
486
502
  }
487
- let publicKey = await this.getPublicKey(kid);
488
- if (!publicKey) {
489
- await this.refreshJwks();
490
- publicKey = await this.getPublicKey(kid);
503
+ let cache = await this.ensureCache();
504
+ if (!cache.byKid.has(kid)) {
505
+ this.jwksCache = null;
506
+ cache = await this.ensureCache();
491
507
  }
492
- if (!publicKey) {
508
+ if (!cache.byKid.has(kid)) {
493
509
  throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
494
510
  }
495
511
  const issuer = options.issuer ?? this.defaultIssuer;
496
512
  const audience = options.audience ?? this.defaultAudience;
497
513
  const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
498
- const algorithms = options.algorithms ?? ["RS256"];
514
+ const algorithms = options.algorithms ?? ["RS256", "ES256"];
515
+ const verifyOptions = {
516
+ algorithms,
517
+ clockTolerance,
518
+ issuer,
519
+ audience
520
+ };
499
521
  try {
500
- const verifyOptions = {
501
- algorithms,
502
- clockTolerance,
503
- // The jsonwebtoken types insist on tuple types for arrays; runtime
504
- // accepts plain string[] so we cast to satisfy the compiler.
505
- issuer,
506
- audience
507
- };
508
- const verified = import_jsonwebtoken.default.verify(token, publicKey, verifyOptions);
509
- return verified;
522
+ const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
523
+ return payload;
510
524
  } catch (err) {
525
+ if (err instanceof import_jose.errors.JWTExpired) {
526
+ throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
527
+ }
528
+ if (err instanceof import_jose.errors.JOSEError) {
529
+ throw new IQAuthError("TOKEN_INVALID", err.message);
530
+ }
511
531
  if (err instanceof Error) {
512
- if (err.name === "TokenExpiredError") {
513
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
514
- }
515
532
  throw new IQAuthError("TOKEN_INVALID", err.message);
516
533
  }
517
534
  throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
@@ -519,29 +536,40 @@ var TokensModule = class {
519
536
  }
520
537
  /**
521
538
  * Decode a JWT without verification. Returns null if malformed.
522
- *
523
- * @remarks Local decode only — no network call
524
539
  */
525
540
  decode(token) {
526
- const decoded = import_jsonwebtoken.default.decode(token);
527
- return decoded;
541
+ try {
542
+ const parts = token.split(".");
543
+ if (parts.length < 2) return null;
544
+ const payload = parts[1];
545
+ const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
546
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
547
+ let json;
548
+ if (typeof atob === "function") {
549
+ json = atob(b64);
550
+ } else {
551
+ const { Buffer: Buffer2 } = require("buffer");
552
+ json = Buffer2.from(b64, "base64").toString("utf8");
553
+ }
554
+ try {
555
+ json = decodeURIComponent(escape(json));
556
+ } catch {
557
+ }
558
+ const claims = JSON.parse(json);
559
+ if (!claims || typeof claims !== "object") return null;
560
+ return claims;
561
+ } catch {
562
+ return null;
563
+ }
528
564
  }
529
- /**
530
- * Check if a token is expired based on the `exp` claim.
531
- *
532
- * @remarks Local check only — no network call
533
- */
565
+ /** Check if a token is expired based on the `exp` claim. */
534
566
  isExpired(token) {
535
567
  const claims = this.decode(token);
536
568
  if (!claims?.exp) return true;
537
569
  const now = Math.floor(Date.now() / 1e3);
538
570
  return claims.exp <= now;
539
571
  }
540
- /**
541
- * Get the claims from a token without verification.
542
- *
543
- * @remarks Local decode only — no network call
544
- */
572
+ /** Get the claims from a token without verification. */
545
573
  getClaims(token) {
546
574
  const claims = this.decode(token);
547
575
  if (!claims) {
@@ -549,11 +577,15 @@ var TokensModule = class {
549
577
  }
550
578
  return claims;
551
579
  }
552
- async getPublicKey(kid) {
553
- if (!this.jwksCache || Date.now() - this.jwksCache.fetchedAt > JWKS_CACHE_TTL_MS) {
554
- await this.refreshJwks();
580
+ async ensureCache() {
581
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
582
+ return this.jwksCache;
583
+ }
584
+ await this.refreshJwks();
585
+ if (!this.jwksCache) {
586
+ throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
555
587
  }
556
- return this.jwksCache?.keys.get(kid) ?? null;
588
+ return this.jwksCache;
557
589
  }
558
590
  async refreshJwks() {
559
591
  if (this.inFlightRefresh) {
@@ -580,35 +612,24 @@ var TokensModule = class {
580
612
  "Malformed JWKS response: expected { keys: [...] }"
581
613
  );
582
614
  }
583
- const keys = /* @__PURE__ */ new Map();
615
+ const byKid = /* @__PURE__ */ new Set();
584
616
  for (const key of jwks.keys) {
585
- if (!key || typeof key.kid !== "string" || typeof key.n !== "string" || typeof key.e !== "string") {
617
+ if (!key || typeof key.kid !== "string" || typeof key.n !== "string" && typeof key.x !== "string" || key.kty === "RSA" && (typeof key.n !== "string" || typeof key.e !== "string")) {
586
618
  throw new IQAuthError(
587
619
  "INTERNAL_ERROR",
588
620
  "Malformed JWKS response: key missing required fields"
589
621
  );
590
622
  }
591
- const pem = this.jwkToPem(key);
592
- keys.set(key.kid, pem);
623
+ byKid.add(key.kid);
593
624
  }
594
- this.jwksCache = { keys, fetchedAt: Date.now() };
625
+ const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
626
+ this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
595
627
  } finally {
596
628
  this.inFlightRefresh = null;
597
629
  }
598
630
  })();
599
631
  return this.inFlightRefresh;
600
632
  }
601
- jwkToPem(jwk) {
602
- const keyObject = import_crypto.default.createPublicKey({
603
- key: {
604
- kty: jwk.kty,
605
- n: jwk.n,
606
- e: jwk.e
607
- },
608
- format: "jwk"
609
- });
610
- return keyObject.export({ type: "spki", format: "pem" });
611
- }
612
633
  /** @internal Exposed for testing — clears JWKS cache */
613
634
  clearCache() {
614
635
  this.jwksCache = null;
@@ -816,7 +837,7 @@ var PermissionsModule = class {
816
837
  };
817
838
 
818
839
  // src/modules/oidc.ts
819
- var import_crypto2 = __toESM(require("crypto"));
840
+ var import_crypto = __toESM(require("crypto"));
820
841
  var InMemoryOidcStateStore = class {
821
842
  constructor() {
822
843
  this.map = /* @__PURE__ */ new Map();
@@ -897,12 +918,12 @@ var OidcModule = class {
897
918
  * ready to redirect the user to.
898
919
  */
899
920
  async createAuthRequest(params) {
900
- const codeVerifier = base64UrlEncode(import_crypto2.default.randomBytes(32));
921
+ const codeVerifier = base64UrlEncode(import_crypto.default.randomBytes(32));
901
922
  const codeChallenge = base64UrlEncode(
902
- import_crypto2.default.createHash("sha256").update(codeVerifier).digest()
923
+ import_crypto.default.createHash("sha256").update(codeVerifier).digest()
903
924
  );
904
- const state = base64UrlEncode(import_crypto2.default.randomBytes(16));
905
- const nonce = base64UrlEncode(import_crypto2.default.randomBytes(16));
925
+ const state = base64UrlEncode(import_crypto.default.randomBytes(16));
926
+ const nonce = base64UrlEncode(import_crypto.default.randomBytes(16));
906
927
  await this.stateStore.set(state, {
907
928
  codeVerifier,
908
929
  state,
@@ -1850,7 +1871,7 @@ function assertPublishableKey(raw, opts) {
1850
1871
  if (!isValidIssuerUrl(decoded.iss)) {
1851
1872
  throw new IQAuthError(
1852
1873
  "CONFIG_INVALID",
1853
- `${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, or set IQAUTH_ISSUER to the correct issuer URL as a temporary workaround.`
1874
+ `${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.`
1854
1875
  );
1855
1876
  }
1856
1877
  return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
@@ -2200,6 +2221,15 @@ async function handleSignout(config, input) {
2200
2221
  } catch {
2201
2222
  }
2202
2223
  }
2224
+ if (input.endSsoSession !== false && input.ssoCookieHeader) {
2225
+ try {
2226
+ await cfg.fetchImpl(`${cfg.issuer}/oidc/sso-logout`, {
2227
+ method: "POST",
2228
+ headers: { Cookie: input.ssoCookieHeader }
2229
+ });
2230
+ } catch {
2231
+ }
2232
+ }
2203
2233
  return {
2204
2234
  status: 200,
2205
2235
  body: { success: true, data: { signedOut: true } },
@@ -2208,6 +2238,78 @@ async function handleSignout(config, input) {
2208
2238
  }
2209
2239
 
2210
2240
  // src/express.ts
2241
+ var PKCE_COOKIE = "iqauth_pkce";
2242
+ function escapeHtml(s) {
2243
+ return s.replace(/[&<>"']/g, (c) => {
2244
+ switch (c) {
2245
+ case "&":
2246
+ return "&amp;";
2247
+ case "<":
2248
+ return "&lt;";
2249
+ case ">":
2250
+ return "&gt;";
2251
+ case '"':
2252
+ return "&quot;";
2253
+ case "'":
2254
+ return "&#39;";
2255
+ default:
2256
+ return c;
2257
+ }
2258
+ });
2259
+ }
2260
+ function appendErrorParam(path, errorCode) {
2261
+ const sep = path.includes("?") ? "&" : "?";
2262
+ return `${path}${sep}error=${encodeURIComponent(errorCode)}`;
2263
+ }
2264
+ function defaultBrandedSpinner(args) {
2265
+ return `<!doctype html>
2266
+ <html lang="en">
2267
+ <head>
2268
+ <meta charset="utf-8" />
2269
+ <title>Signing you in\u2026</title>
2270
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
2271
+ <style>
2272
+ body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; background:#f7f7f8; color:#111; }
2273
+ .iqauth-card { text-align:center; padding:2rem; }
2274
+ .iqauth-spinner { width:36px; height:36px; border:3px solid #e5e7eb; border-top-color:#111; border-radius:50%; margin:0 auto 1rem; animation:iqauth-spin 0.9s linear infinite; }
2275
+ @keyframes iqauth-spin { to { transform: rotate(360deg); } }
2276
+ .iqauth-msg { font-size:0.95rem; color:#374151; }
2277
+ </style>
2278
+ </head>
2279
+ <body>
2280
+ <div class="iqauth-card" data-testid="iqauth-inline-callback-spinner">
2281
+ <div class="iqauth-spinner" aria-hidden="true"></div>
2282
+ <div class="iqauth-msg">Signing you in\u2026</div>
2283
+ </div>
2284
+ <script>
2285
+ (function(){
2286
+ var code = ${JSON.stringify(args.code)};
2287
+ var state = ${JSON.stringify(args.state)};
2288
+ var errorPath = ${JSON.stringify(args.errorPath || "")};
2289
+ function fail(reason){
2290
+ if (errorPath) { window.location.replace(errorPath + (errorPath.indexOf("?")>=0?"&":"?") + "error=" + encodeURIComponent(reason)); return; }
2291
+ window.location.replace("/");
2292
+ }
2293
+ var verifier = (document.cookie.split('; ').find(function(c){return c.indexOf('${PKCE_COOKIE}=')===0;})||'').slice(${PKCE_COOKIE.length + 1});
2294
+ try { verifier = decodeURIComponent(verifier); } catch (e) {}
2295
+ fetch(${JSON.stringify(args.exchangePath)}, {
2296
+ method: "POST",
2297
+ credentials: "include",
2298
+ headers: { "Content-Type": "application/json" },
2299
+ body: JSON.stringify({ code: code, state: state, codeVerifier: verifier, redirectUri: window.location.origin + window.location.pathname })
2300
+ }).then(function(r){ return r.json().then(function(j){ return { status:r.status, body:j }; }); })
2301
+ .then(function(out){
2302
+ if (out.status >= 400) { fail((out.body && out.body.error && out.body.error.code) || "exchange_failed"); return; }
2303
+ var dest = (out.body && out.body.returnTo) || sessionStorage.getItem("iqauth_return_to") || "/";
2304
+ sessionStorage.removeItem("iqauth_return_to");
2305
+ window.location.replace(dest);
2306
+ })
2307
+ .catch(function(){ fail("network_error"); });
2308
+ })();
2309
+ </script>
2310
+ </body>
2311
+ </html>`;
2312
+ }
2211
2313
  function applyHandlerResponse(res, hr) {
2212
2314
  for (const c of hr.cookies) {
2213
2315
  if (typeof res.cookie === "function") {
@@ -2271,6 +2373,8 @@ function iqAuth(options) {
2271
2373
  if (mountHelpers && path.startsWith(mount + "/")) return next();
2272
2374
  return verify(req, res, next);
2273
2375
  };
2376
+ const inline = options.inlineCallback === true ? {} : options.inlineCallback && typeof options.inlineCallback === "object" ? options.inlineCallback : null;
2377
+ const inlineBranded = inline?.branded === true ? {} : inline?.branded && typeof inline.branded === "object" ? inline.branded : null;
2274
2378
  const attachHelpers = (app) => {
2275
2379
  app.post(`${mount}/callback`, async (req, res) => {
2276
2380
  const body = readBody(req);
@@ -2281,6 +2385,131 @@ function iqAuth(options) {
2281
2385
  });
2282
2386
  applyHandlerResponse(res, hr);
2283
2387
  });
2388
+ if (inline && typeof app.get === "function") {
2389
+ const callbackPath = `${mount}/callback`;
2390
+ const exchangePath = `${callbackPath}/exchange`;
2391
+ const stateCookie = inline.stateCookieName ?? "iqauth_state";
2392
+ const returnToCookie = inline.returnToCookieName ?? "iqauth_return_to";
2393
+ const errorPath = inline.errorPath;
2394
+ const clearCookie = (res, name) => {
2395
+ if (typeof res.clearCookie === "function") {
2396
+ res.clearCookie(name, { path: "/" });
2397
+ return;
2398
+ }
2399
+ const existing = res.getHeader?.("Set-Cookie") || [];
2400
+ const list = Array.isArray(existing) ? existing : [existing];
2401
+ list.push(`${name}=; Path=/; Max-Age=0; SameSite=Lax`);
2402
+ res.setHeader?.("Set-Cookie", list);
2403
+ };
2404
+ const failPlain = (res, errorCode, fallback) => {
2405
+ if (errorPath) {
2406
+ const dest = appendErrorParam(errorPath, errorCode);
2407
+ if (typeof res.redirect === "function") return res.redirect(302, dest);
2408
+ res.status(302);
2409
+ res.setHeader?.("Location", dest);
2410
+ return res.end?.();
2411
+ }
2412
+ fallback();
2413
+ };
2414
+ if (inlineBranded) {
2415
+ const render = inlineBranded.render ?? defaultBrandedSpinner;
2416
+ app.get(callbackPath, (req, res) => {
2417
+ const q = req.query || {};
2418
+ const html = render({
2419
+ issuer,
2420
+ exchangePath,
2421
+ code: escapeHtml(q.code ?? ""),
2422
+ state: escapeHtml(q.state ?? ""),
2423
+ errorPath: errorPath ?? ""
2424
+ });
2425
+ res.status(200);
2426
+ if (typeof res.set === "function") res.set("Content-Type", "text/html; charset=utf-8");
2427
+ else if (typeof res.setHeader === "function") res.setHeader("Content-Type", "text/html; charset=utf-8");
2428
+ if (typeof res.send === "function") res.send(html);
2429
+ else res.end?.(html);
2430
+ });
2431
+ app.post(exchangePath, async (req, res) => {
2432
+ const body = readBody(req);
2433
+ const stateFromBody = body.state || void 0;
2434
+ const stateFromCookie = readCookieFromReq(req, stateCookie);
2435
+ if (stateFromCookie && stateFromBody !== stateFromCookie) {
2436
+ clearCookie(res, stateCookie);
2437
+ res.status(400);
2438
+ return res.json ? res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } }) : res.end?.(JSON.stringify({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } }));
2439
+ }
2440
+ const hr = await handleCallback(helperConfig, {
2441
+ code: body.code,
2442
+ codeVerifier: body.codeVerifier || readCookieFromReq(req, PKCE_COOKIE) || "",
2443
+ redirectUri: body.redirectUri
2444
+ });
2445
+ clearCookie(res, stateCookie);
2446
+ clearCookie(res, PKCE_COOKIE);
2447
+ const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
2448
+ if (hr.status < 400) clearCookie(res, returnToCookie);
2449
+ const enriched = {
2450
+ ...hr,
2451
+ body: { ...hr.body, returnTo }
2452
+ };
2453
+ applyHandlerResponse(res, enriched);
2454
+ });
2455
+ } else {
2456
+ app.get(callbackPath, async (req, res) => {
2457
+ const q = req.query || {};
2458
+ const code = q.code;
2459
+ if (!code) {
2460
+ return failPlain(res, "missing_code", () => {
2461
+ res.status(400);
2462
+ if (res.json) res.json({ success: false, error: { code: "MISSING_CODE", message: "Missing authorization code" } });
2463
+ else res.end?.("Missing authorization code");
2464
+ });
2465
+ }
2466
+ const stateFromQuery = q.state;
2467
+ const stateFromCookie = readCookieFromReq(req, stateCookie);
2468
+ if (stateFromCookie && stateFromQuery !== stateFromCookie) {
2469
+ clearCookie(res, stateCookie);
2470
+ return failPlain(res, "state_mismatch", () => {
2471
+ res.status(400);
2472
+ if (res.json) res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } });
2473
+ else res.end?.("OAuth state mismatch");
2474
+ });
2475
+ }
2476
+ const codeVerifier = readCookieFromReq(req, PKCE_COOKIE) || "";
2477
+ const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "https";
2478
+ const host = req.headers?.["x-forwarded-host"] || req.headers?.host || "";
2479
+ const redirectUri = `${proto}://${host}${callbackPath}`;
2480
+ const hr = await handleCallback(helperConfig, { code, codeVerifier, redirectUri });
2481
+ for (const c of hr.cookies) {
2482
+ if (typeof res.cookie === "function") {
2483
+ const opts = {
2484
+ httpOnly: c.httpOnly,
2485
+ secure: c.secure,
2486
+ sameSite: c.sameSite,
2487
+ path: c.path,
2488
+ maxAge: c.maxAge * 1e3
2489
+ };
2490
+ if (c.domain) opts.domain = c.domain;
2491
+ res.cookie(c.name, c.value, opts);
2492
+ }
2493
+ }
2494
+ clearCookie(res, stateCookie);
2495
+ clearCookie(res, PKCE_COOKIE);
2496
+ if (hr.status >= 400) {
2497
+ const code2 = hr.body?.error?.code || "exchange_failed";
2498
+ return failPlain(res, code2, () => {
2499
+ res.status(hr.status);
2500
+ if (res.json) res.json(hr.body);
2501
+ else res.end?.(JSON.stringify(hr.body));
2502
+ });
2503
+ }
2504
+ const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
2505
+ clearCookie(res, returnToCookie);
2506
+ if (typeof res.redirect === "function") return res.redirect(302, returnTo);
2507
+ res.status(302);
2508
+ if (typeof res.setHeader === "function") res.setHeader("Location", returnTo);
2509
+ res.end?.();
2510
+ });
2511
+ }
2512
+ }
2284
2513
  app.post(`${mount}/refresh`, async (req, res) => {
2285
2514
  const body = readBody(req);
2286
2515
  const refreshToken = body.refreshToken || readCookieFromReq(req, refreshCookie);
@@ -2289,7 +2518,8 @@ function iqAuth(options) {
2289
2518
  });
2290
2519
  app.post(`${mount}/signout`, async (req, res) => {
2291
2520
  const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
2292
- const hr = await handleSignout(helperConfig, { accessToken });
2521
+ const ssoCookieHeader = req.headers?.cookie;
2522
+ const hr = await handleSignout(helperConfig, { accessToken, ssoCookieHeader });
2293
2523
  applyHandlerResponse(res, hr);
2294
2524
  });
2295
2525
  };