@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.mjs CHANGED
@@ -1,18 +1,19 @@
1
1
  import {
2
2
  DEFAULT_REFRESH_COOKIE,
3
3
  iqAuthMiddleware
4
- } from "./chunk-D72UL5HL.mjs";
4
+ } from "./chunk-EKTNEZIH.mjs";
5
+ import {
6
+ IQAuthClient
7
+ } from "./chunk-W3F4JYGP.mjs";
5
8
  import {
6
9
  handleCallback,
7
10
  handleRefresh,
8
11
  handleSignout
9
- } from "./chunk-M4J6BPK7.mjs";
12
+ } from "./chunk-KGEPDXHU.mjs";
10
13
  import {
11
14
  assertPublishableKey
12
- } from "./chunk-QEJB7WEQ.mjs";
13
- import {
14
- IQAuthClient
15
- } from "./chunk-MDUHPQMM.mjs";
15
+ } from "./chunk-WQWBJSSS.mjs";
16
+ import "./chunk-UNYDG2L4.mjs";
16
17
  import {
17
18
  ErrorCodes,
18
19
  IQAuthError
@@ -20,6 +21,78 @@ import {
20
21
  import "./chunk-Y6FXYEAI.mjs";
21
22
 
22
23
  // src/express.ts
24
+ var PKCE_COOKIE = "iqauth_pkce";
25
+ function escapeHtml(s) {
26
+ return s.replace(/[&<>"']/g, (c) => {
27
+ switch (c) {
28
+ case "&":
29
+ return "&amp;";
30
+ case "<":
31
+ return "&lt;";
32
+ case ">":
33
+ return "&gt;";
34
+ case '"':
35
+ return "&quot;";
36
+ case "'":
37
+ return "&#39;";
38
+ default:
39
+ return c;
40
+ }
41
+ });
42
+ }
43
+ function appendErrorParam(path, errorCode) {
44
+ const sep = path.includes("?") ? "&" : "?";
45
+ return `${path}${sep}error=${encodeURIComponent(errorCode)}`;
46
+ }
47
+ function defaultBrandedSpinner(args) {
48
+ return `<!doctype html>
49
+ <html lang="en">
50
+ <head>
51
+ <meta charset="utf-8" />
52
+ <title>Signing you in\u2026</title>
53
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
54
+ <style>
55
+ 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; }
56
+ .iqauth-card { text-align:center; padding:2rem; }
57
+ .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; }
58
+ @keyframes iqauth-spin { to { transform: rotate(360deg); } }
59
+ .iqauth-msg { font-size:0.95rem; color:#374151; }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <div class="iqauth-card" data-testid="iqauth-inline-callback-spinner">
64
+ <div class="iqauth-spinner" aria-hidden="true"></div>
65
+ <div class="iqauth-msg">Signing you in\u2026</div>
66
+ </div>
67
+ <script>
68
+ (function(){
69
+ var code = ${JSON.stringify(args.code)};
70
+ var state = ${JSON.stringify(args.state)};
71
+ var errorPath = ${JSON.stringify(args.errorPath || "")};
72
+ function fail(reason){
73
+ if (errorPath) { window.location.replace(errorPath + (errorPath.indexOf("?")>=0?"&":"?") + "error=" + encodeURIComponent(reason)); return; }
74
+ window.location.replace("/");
75
+ }
76
+ var verifier = (document.cookie.split('; ').find(function(c){return c.indexOf('${PKCE_COOKIE}=')===0;})||'').slice(${PKCE_COOKIE.length + 1});
77
+ try { verifier = decodeURIComponent(verifier); } catch (e) {}
78
+ fetch(${JSON.stringify(args.exchangePath)}, {
79
+ method: "POST",
80
+ credentials: "include",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify({ code: code, state: state, codeVerifier: verifier, redirectUri: window.location.origin + window.location.pathname })
83
+ }).then(function(r){ return r.json().then(function(j){ return { status:r.status, body:j }; }); })
84
+ .then(function(out){
85
+ if (out.status >= 400) { fail((out.body && out.body.error && out.body.error.code) || "exchange_failed"); return; }
86
+ var dest = (out.body && out.body.returnTo) || sessionStorage.getItem("iqauth_return_to") || "/";
87
+ sessionStorage.removeItem("iqauth_return_to");
88
+ window.location.replace(dest);
89
+ })
90
+ .catch(function(){ fail("network_error"); });
91
+ })();
92
+ </script>
93
+ </body>
94
+ </html>`;
95
+ }
23
96
  function applyHandlerResponse(res, hr) {
24
97
  for (const c of hr.cookies) {
25
98
  if (typeof res.cookie === "function") {
@@ -83,6 +156,8 @@ function iqAuth(options) {
83
156
  if (mountHelpers && path.startsWith(mount + "/")) return next();
84
157
  return verify(req, res, next);
85
158
  };
159
+ const inline = options.inlineCallback === true ? {} : options.inlineCallback && typeof options.inlineCallback === "object" ? options.inlineCallback : null;
160
+ const inlineBranded = inline?.branded === true ? {} : inline?.branded && typeof inline.branded === "object" ? inline.branded : null;
86
161
  const attachHelpers = (app) => {
87
162
  app.post(`${mount}/callback`, async (req, res) => {
88
163
  const body = readBody(req);
@@ -93,6 +168,131 @@ function iqAuth(options) {
93
168
  });
94
169
  applyHandlerResponse(res, hr);
95
170
  });
171
+ if (inline && typeof app.get === "function") {
172
+ const callbackPath = `${mount}/callback`;
173
+ const exchangePath = `${callbackPath}/exchange`;
174
+ const stateCookie = inline.stateCookieName ?? "iqauth_state";
175
+ const returnToCookie = inline.returnToCookieName ?? "iqauth_return_to";
176
+ const errorPath = inline.errorPath;
177
+ const clearCookie = (res, name) => {
178
+ if (typeof res.clearCookie === "function") {
179
+ res.clearCookie(name, { path: "/" });
180
+ return;
181
+ }
182
+ const existing = res.getHeader?.("Set-Cookie") || [];
183
+ const list = Array.isArray(existing) ? existing : [existing];
184
+ list.push(`${name}=; Path=/; Max-Age=0; SameSite=Lax`);
185
+ res.setHeader?.("Set-Cookie", list);
186
+ };
187
+ const failPlain = (res, errorCode, fallback) => {
188
+ if (errorPath) {
189
+ const dest = appendErrorParam(errorPath, errorCode);
190
+ if (typeof res.redirect === "function") return res.redirect(302, dest);
191
+ res.status(302);
192
+ res.setHeader?.("Location", dest);
193
+ return res.end?.();
194
+ }
195
+ fallback();
196
+ };
197
+ if (inlineBranded) {
198
+ const render = inlineBranded.render ?? defaultBrandedSpinner;
199
+ app.get(callbackPath, (req, res) => {
200
+ const q = req.query || {};
201
+ const html = render({
202
+ issuer,
203
+ exchangePath,
204
+ code: escapeHtml(q.code ?? ""),
205
+ state: escapeHtml(q.state ?? ""),
206
+ errorPath: errorPath ?? ""
207
+ });
208
+ res.status(200);
209
+ if (typeof res.set === "function") res.set("Content-Type", "text/html; charset=utf-8");
210
+ else if (typeof res.setHeader === "function") res.setHeader("Content-Type", "text/html; charset=utf-8");
211
+ if (typeof res.send === "function") res.send(html);
212
+ else res.end?.(html);
213
+ });
214
+ app.post(exchangePath, async (req, res) => {
215
+ const body = readBody(req);
216
+ const stateFromBody = body.state || void 0;
217
+ const stateFromCookie = readCookieFromReq(req, stateCookie);
218
+ if (stateFromCookie && stateFromBody !== stateFromCookie) {
219
+ clearCookie(res, stateCookie);
220
+ res.status(400);
221
+ 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" } }));
222
+ }
223
+ const hr = await handleCallback(helperConfig, {
224
+ code: body.code,
225
+ codeVerifier: body.codeVerifier || readCookieFromReq(req, PKCE_COOKIE) || "",
226
+ redirectUri: body.redirectUri
227
+ });
228
+ clearCookie(res, stateCookie);
229
+ clearCookie(res, PKCE_COOKIE);
230
+ const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
231
+ if (hr.status < 400) clearCookie(res, returnToCookie);
232
+ const enriched = {
233
+ ...hr,
234
+ body: { ...hr.body, returnTo }
235
+ };
236
+ applyHandlerResponse(res, enriched);
237
+ });
238
+ } else {
239
+ app.get(callbackPath, async (req, res) => {
240
+ const q = req.query || {};
241
+ const code = q.code;
242
+ if (!code) {
243
+ return failPlain(res, "missing_code", () => {
244
+ res.status(400);
245
+ if (res.json) res.json({ success: false, error: { code: "MISSING_CODE", message: "Missing authorization code" } });
246
+ else res.end?.("Missing authorization code");
247
+ });
248
+ }
249
+ const stateFromQuery = q.state;
250
+ const stateFromCookie = readCookieFromReq(req, stateCookie);
251
+ if (stateFromCookie && stateFromQuery !== stateFromCookie) {
252
+ clearCookie(res, stateCookie);
253
+ return failPlain(res, "state_mismatch", () => {
254
+ res.status(400);
255
+ if (res.json) res.json({ success: false, error: { code: "STATE_MISMATCH", message: "OAuth state mismatch" } });
256
+ else res.end?.("OAuth state mismatch");
257
+ });
258
+ }
259
+ const codeVerifier = readCookieFromReq(req, PKCE_COOKIE) || "";
260
+ const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "https";
261
+ const host = req.headers?.["x-forwarded-host"] || req.headers?.host || "";
262
+ const redirectUri = `${proto}://${host}${callbackPath}`;
263
+ const hr = await handleCallback(helperConfig, { code, codeVerifier, redirectUri });
264
+ for (const c of hr.cookies) {
265
+ if (typeof res.cookie === "function") {
266
+ const opts = {
267
+ httpOnly: c.httpOnly,
268
+ secure: c.secure,
269
+ sameSite: c.sameSite,
270
+ path: c.path,
271
+ maxAge: c.maxAge * 1e3
272
+ };
273
+ if (c.domain) opts.domain = c.domain;
274
+ res.cookie(c.name, c.value, opts);
275
+ }
276
+ }
277
+ clearCookie(res, stateCookie);
278
+ clearCookie(res, PKCE_COOKIE);
279
+ if (hr.status >= 400) {
280
+ const code2 = hr.body?.error?.code || "exchange_failed";
281
+ return failPlain(res, code2, () => {
282
+ res.status(hr.status);
283
+ if (res.json) res.json(hr.body);
284
+ else res.end?.(JSON.stringify(hr.body));
285
+ });
286
+ }
287
+ const returnTo = readCookieFromReq(req, returnToCookie) || hr.body?.returnTo || "/";
288
+ clearCookie(res, returnToCookie);
289
+ if (typeof res.redirect === "function") return res.redirect(302, returnTo);
290
+ res.status(302);
291
+ if (typeof res.setHeader === "function") res.setHeader("Location", returnTo);
292
+ res.end?.();
293
+ });
294
+ }
295
+ }
96
296
  app.post(`${mount}/refresh`, async (req, res) => {
97
297
  const body = readBody(req);
98
298
  const refreshToken = body.refreshToken || readCookieFromReq(req, refreshCookie);
@@ -101,7 +301,8 @@ function iqAuth(options) {
101
301
  });
102
302
  app.post(`${mount}/signout`, async (req, res) => {
103
303
  const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
104
- const hr = await handleSignout(helperConfig, { accessToken });
304
+ const ssoCookieHeader = req.headers?.cookie;
305
+ const hr = await handleSignout(helperConfig, { accessToken, ssoCookieHeader });
105
306
  applyHandlerResponse(res, hr);
106
307
  });
107
308
  };
package/dist/fastify.js CHANGED
@@ -407,8 +407,7 @@ function parseMfaResponse(data, browserSessionMode) {
407
407
  }
408
408
 
409
409
  // src/modules/tokens.ts
410
- var import_crypto = __toESM(require("crypto"));
411
- var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
410
+ var import_jose = require("jose");
412
411
  var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
413
412
  var DEFAULT_TOKEN_ISSUER = [
414
413
  "https://auth.dispositioniq.com",
@@ -421,6 +420,24 @@ var DEFAULT_TOKEN_AUDIENCE = [
421
420
  "iqvalidate"
422
421
  ];
423
422
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
423
+ function decodeProtectedHeader(token) {
424
+ const parts = token.split(".");
425
+ if (parts.length < 2) return null;
426
+ try {
427
+ const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
428
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
429
+ let json;
430
+ if (typeof atob === "function") {
431
+ json = atob(b64);
432
+ } else {
433
+ const { Buffer: Buffer2 } = require("buffer");
434
+ json = Buffer2.from(b64, "base64").toString("utf8");
435
+ }
436
+ return JSON.parse(json);
437
+ } catch {
438
+ return null;
439
+ }
440
+ }
424
441
  var TokensModule = class {
425
442
  constructor(baseUrl, options = {}) {
426
443
  this.jwksCache = null;
@@ -431,49 +448,49 @@ var TokensModule = class {
431
448
  this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
432
449
  }
433
450
  /**
434
- * Verify a JWT access token using RS256 via JWKS from /.well-known/jwks.json.
435
- * Caches JWKS keys for 1 hour. Retries once on unknown `kid`.
436
- *
437
- * @remarks Validates against /.well-known/jwks.json. Issuer, audience, and
438
- * clock tolerance default to client config but can be overridden per call.
451
+ * Verify a JWT access token using RS256/ES256 via JWKS from
452
+ * `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
453
+ * Node, browser, and edge runtimes alike — no `node:crypto` dependency.
454
+ * Caches JWKS for 1 hour and refetches once on unknown `kid`.
439
455
  */
440
456
  async verify(token, options = {}) {
441
- const decoded = import_jsonwebtoken.default.decode(token, { complete: true });
442
- if (!decoded || typeof decoded === "string") {
457
+ const header = decodeProtectedHeader(token);
458
+ if (!header) {
443
459
  throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
444
460
  }
445
- const kid = decoded.header.kid;
461
+ const kid = header.kid;
446
462
  if (!kid) {
447
463
  throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
448
464
  }
449
- let publicKey = await this.getPublicKey(kid);
450
- if (!publicKey) {
451
- await this.refreshJwks();
452
- publicKey = await this.getPublicKey(kid);
465
+ let cache = await this.ensureCache();
466
+ if (!cache.byKid.has(kid)) {
467
+ this.jwksCache = null;
468
+ cache = await this.ensureCache();
453
469
  }
454
- if (!publicKey) {
470
+ if (!cache.byKid.has(kid)) {
455
471
  throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
456
472
  }
457
473
  const issuer = options.issuer ?? this.defaultIssuer;
458
474
  const audience = options.audience ?? this.defaultAudience;
459
475
  const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
460
- const algorithms = options.algorithms ?? ["RS256"];
476
+ const algorithms = options.algorithms ?? ["RS256", "ES256"];
477
+ const verifyOptions = {
478
+ algorithms,
479
+ clockTolerance,
480
+ issuer,
481
+ audience
482
+ };
461
483
  try {
462
- const verifyOptions = {
463
- algorithms,
464
- clockTolerance,
465
- // The jsonwebtoken types insist on tuple types for arrays; runtime
466
- // accepts plain string[] so we cast to satisfy the compiler.
467
- issuer,
468
- audience
469
- };
470
- const verified = import_jsonwebtoken.default.verify(token, publicKey, verifyOptions);
471
- return verified;
484
+ const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
485
+ return payload;
472
486
  } catch (err) {
487
+ if (err instanceof import_jose.errors.JWTExpired) {
488
+ throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
489
+ }
490
+ if (err instanceof import_jose.errors.JOSEError) {
491
+ throw new IQAuthError("TOKEN_INVALID", err.message);
492
+ }
473
493
  if (err instanceof Error) {
474
- if (err.name === "TokenExpiredError") {
475
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
476
- }
477
494
  throw new IQAuthError("TOKEN_INVALID", err.message);
478
495
  }
479
496
  throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
@@ -481,29 +498,40 @@ var TokensModule = class {
481
498
  }
482
499
  /**
483
500
  * Decode a JWT without verification. Returns null if malformed.
484
- *
485
- * @remarks Local decode only — no network call
486
501
  */
487
502
  decode(token) {
488
- const decoded = import_jsonwebtoken.default.decode(token);
489
- return decoded;
503
+ try {
504
+ const parts = token.split(".");
505
+ if (parts.length < 2) return null;
506
+ const payload = parts[1];
507
+ const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
508
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
509
+ let json;
510
+ if (typeof atob === "function") {
511
+ json = atob(b64);
512
+ } else {
513
+ const { Buffer: Buffer2 } = require("buffer");
514
+ json = Buffer2.from(b64, "base64").toString("utf8");
515
+ }
516
+ try {
517
+ json = decodeURIComponent(escape(json));
518
+ } catch {
519
+ }
520
+ const claims = JSON.parse(json);
521
+ if (!claims || typeof claims !== "object") return null;
522
+ return claims;
523
+ } catch {
524
+ return null;
525
+ }
490
526
  }
491
- /**
492
- * Check if a token is expired based on the `exp` claim.
493
- *
494
- * @remarks Local check only — no network call
495
- */
527
+ /** Check if a token is expired based on the `exp` claim. */
496
528
  isExpired(token) {
497
529
  const claims = this.decode(token);
498
530
  if (!claims?.exp) return true;
499
531
  const now = Math.floor(Date.now() / 1e3);
500
532
  return claims.exp <= now;
501
533
  }
502
- /**
503
- * Get the claims from a token without verification.
504
- *
505
- * @remarks Local decode only — no network call
506
- */
534
+ /** Get the claims from a token without verification. */
507
535
  getClaims(token) {
508
536
  const claims = this.decode(token);
509
537
  if (!claims) {
@@ -511,11 +539,15 @@ var TokensModule = class {
511
539
  }
512
540
  return claims;
513
541
  }
514
- async getPublicKey(kid) {
515
- if (!this.jwksCache || Date.now() - this.jwksCache.fetchedAt > JWKS_CACHE_TTL_MS) {
516
- await this.refreshJwks();
542
+ async ensureCache() {
543
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
544
+ return this.jwksCache;
545
+ }
546
+ await this.refreshJwks();
547
+ if (!this.jwksCache) {
548
+ throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
517
549
  }
518
- return this.jwksCache?.keys.get(kid) ?? null;
550
+ return this.jwksCache;
519
551
  }
520
552
  async refreshJwks() {
521
553
  if (this.inFlightRefresh) {
@@ -542,35 +574,24 @@ var TokensModule = class {
542
574
  "Malformed JWKS response: expected { keys: [...] }"
543
575
  );
544
576
  }
545
- const keys = /* @__PURE__ */ new Map();
577
+ const byKid = /* @__PURE__ */ new Set();
546
578
  for (const key of jwks.keys) {
547
- if (!key || typeof key.kid !== "string" || typeof key.n !== "string" || typeof key.e !== "string") {
579
+ 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")) {
548
580
  throw new IQAuthError(
549
581
  "INTERNAL_ERROR",
550
582
  "Malformed JWKS response: key missing required fields"
551
583
  );
552
584
  }
553
- const pem = this.jwkToPem(key);
554
- keys.set(key.kid, pem);
585
+ byKid.add(key.kid);
555
586
  }
556
- this.jwksCache = { keys, fetchedAt: Date.now() };
587
+ const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
588
+ this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
557
589
  } finally {
558
590
  this.inFlightRefresh = null;
559
591
  }
560
592
  })();
561
593
  return this.inFlightRefresh;
562
594
  }
563
- jwkToPem(jwk) {
564
- const keyObject = import_crypto.default.createPublicKey({
565
- key: {
566
- kty: jwk.kty,
567
- n: jwk.n,
568
- e: jwk.e
569
- },
570
- format: "jwk"
571
- });
572
- return keyObject.export({ type: "spki", format: "pem" });
573
- }
574
595
  /** @internal Exposed for testing — clears JWKS cache */
575
596
  clearCache() {
576
597
  this.jwksCache = null;
@@ -778,7 +799,7 @@ var PermissionsModule = class {
778
799
  };
779
800
 
780
801
  // src/modules/oidc.ts
781
- var import_crypto2 = __toESM(require("crypto"));
802
+ var import_crypto = __toESM(require("crypto"));
782
803
  var InMemoryOidcStateStore = class {
783
804
  constructor() {
784
805
  this.map = /* @__PURE__ */ new Map();
@@ -859,12 +880,12 @@ var OidcModule = class {
859
880
  * ready to redirect the user to.
860
881
  */
861
882
  async createAuthRequest(params) {
862
- const codeVerifier = base64UrlEncode(import_crypto2.default.randomBytes(32));
883
+ const codeVerifier = base64UrlEncode(import_crypto.default.randomBytes(32));
863
884
  const codeChallenge = base64UrlEncode(
864
- import_crypto2.default.createHash("sha256").update(codeVerifier).digest()
885
+ import_crypto.default.createHash("sha256").update(codeVerifier).digest()
865
886
  );
866
- const state = base64UrlEncode(import_crypto2.default.randomBytes(16));
867
- const nonce = base64UrlEncode(import_crypto2.default.randomBytes(16));
887
+ const state = base64UrlEncode(import_crypto.default.randomBytes(16));
888
+ const nonce = base64UrlEncode(import_crypto.default.randomBytes(16));
868
889
  await this.stateStore.set(state, {
869
890
  codeVerifier,
870
891
  state,
@@ -1812,7 +1833,7 @@ function assertPublishableKey(raw, opts) {
1812
1833
  if (!isValidIssuerUrl(decoded.iss)) {
1813
1834
  throw new IQAuthError(
1814
1835
  "CONFIG_INVALID",
1815
- `${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.`
1836
+ `${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.`
1816
1837
  );
1817
1838
  }
1818
1839
  return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
@@ -2014,6 +2035,15 @@ async function handleSignout(config, input) {
2014
2035
  } catch {
2015
2036
  }
2016
2037
  }
2038
+ if (input.endSsoSession !== false && input.ssoCookieHeader) {
2039
+ try {
2040
+ await cfg.fetchImpl(`${cfg.issuer}/oidc/sso-logout`, {
2041
+ method: "POST",
2042
+ headers: { Cookie: input.ssoCookieHeader }
2043
+ });
2044
+ } catch {
2045
+ }
2046
+ }
2017
2047
  return {
2018
2048
  status: 200,
2019
2049
  body: { success: true, data: { signedOut: true } },
@@ -2120,7 +2150,8 @@ async function iqAuth(fastify, options) {
2120
2150
  fastify.post(`${mount}/signout`, async (req, reply) => {
2121
2151
  const auth = req.headers?.authorization;
2122
2152
  const accessToken = (typeof auth === "string" ? auth.replace(/^Bearer /i, "") : void 0) || readCookie(req, accessCookie);
2123
- applyResponse(reply, await handleSignout(helperConfig, { accessToken }));
2153
+ const ssoCookieHeader = typeof req.headers?.cookie === "string" ? req.headers.cookie : void 0;
2154
+ applyResponse(reply, await handleSignout(helperConfig, { accessToken, ssoCookieHeader }));
2124
2155
  });
2125
2156
  }
2126
2157
  fastify.decorate("iqauth", { client, issuer });
package/dist/fastify.mjs CHANGED
@@ -1,15 +1,16 @@
1
+ import {
2
+ IQAuthClient
3
+ } from "./chunk-W3F4JYGP.mjs";
1
4
  import {
2
5
  handleCallback,
3
6
  handleRefresh,
4
7
  handleSignout,
5
8
  serializeCookie
6
- } from "./chunk-M4J6BPK7.mjs";
9
+ } from "./chunk-KGEPDXHU.mjs";
7
10
  import {
8
11
  assertPublishableKey
9
- } from "./chunk-QEJB7WEQ.mjs";
10
- import {
11
- IQAuthClient
12
- } from "./chunk-MDUHPQMM.mjs";
12
+ } from "./chunk-WQWBJSSS.mjs";
13
+ import "./chunk-UNYDG2L4.mjs";
13
14
  import {
14
15
  IQAuthError
15
16
  } from "./chunk-6I6RM4MN.mjs";
@@ -114,7 +115,8 @@ async function iqAuth(fastify, options) {
114
115
  fastify.post(`${mount}/signout`, async (req, reply) => {
115
116
  const auth = req.headers?.authorization;
116
117
  const accessToken = (typeof auth === "string" ? auth.replace(/^Bearer /i, "") : void 0) || readCookie(req, accessCookie);
117
- applyResponse(reply, await handleSignout(helperConfig, { accessToken }));
118
+ const ssoCookieHeader = typeof req.headers?.cookie === "string" ? req.headers.cookie : void 0;
119
+ applyResponse(reply, await handleSignout(helperConfig, { accessToken, ssoCookieHeader }));
118
120
  });
119
121
  }
120
122
  fastify.decorate("iqauth", { client, issuer });