@iqauth/sdk 2.1.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 (64) hide show
  1. package/README.md +43 -3
  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 +2 -2
  7. package/dist/browser.d.ts +2 -2
  8. package/dist/browser.js +69 -7
  9. package/dist/browser.mjs +2 -2
  10. package/dist/{chunk-ZESHDJDU.mjs → chunk-EKTNEZIH.mjs} +5 -8
  11. package/dist/{chunk-JQRTY5MY.mjs → chunk-KGEPDXHU.mjs} +12 -8
  12. package/dist/{chunk-S3M2IXCE.mjs → chunk-RACIPVLD.mjs} +15 -9
  13. package/dist/chunk-UNYDG2L4.mjs +209 -0
  14. package/dist/{chunk-MDUHPQMM.mjs → chunk-W3F4JYGP.mjs} +8 -180
  15. package/dist/chunk-WQWBJSSS.mjs +119 -0
  16. package/dist/cli/index.js +21 -0
  17. package/dist/cli/index.mjs +1 -1
  18. package/dist/{client-DXbHb2ul.d.ts → client-DTX4hNdS.d.ts} +16 -21
  19. package/dist/{client-Dv4v92Mj.d.mts → client-vdh2a9fJ.d.mts} +16 -21
  20. package/dist/{doctor-OHJRZBBT.mjs → doctor-A5E7LSFW.mjs} +2 -1
  21. package/dist/{express-BZmF1llh.d.mts → express-A0-dWEMy.d.mts} +1 -1
  22. package/dist/{express-B4o3P8vK.d.ts → express-Bo_pJKHN.d.ts} +1 -1
  23. package/dist/express.d.mts +75 -5
  24. package/dist/express.d.ts +75 -5
  25. package/dist/express.js +353 -94
  26. package/dist/express.mjs +210 -12
  27. package/dist/fastify.js +153 -88
  28. package/dist/fastify.mjs +10 -9
  29. package/dist/hono.js +152 -88
  30. package/dist/hono.mjs +9 -9
  31. package/dist/index.d.mts +3 -4
  32. package/dist/index.d.ts +3 -4
  33. package/dist/index.js +148 -72
  34. package/dist/index.mjs +16 -12
  35. package/dist/mobile.d.mts +1 -2
  36. package/dist/mobile.d.ts +1 -2
  37. package/dist/mobile.js +89 -68
  38. package/dist/mobile.mjs +2 -1
  39. package/dist/next.d.mts +9 -0
  40. package/dist/next.d.ts +9 -0
  41. package/dist/next.js +164 -1649
  42. package/dist/next.mjs +13 -16
  43. package/dist/{publishableKey-B5DIK81A.d.mts → publishableKey-BaR0HoAH.d.mts} +10 -1
  44. package/dist/{publishableKey-B5DIK81A.d.ts → publishableKey-BaR0HoAH.d.ts} +10 -1
  45. package/dist/react.d.mts +35 -3
  46. package/dist/react.d.ts +35 -3
  47. package/dist/react.js +78 -18
  48. package/dist/react.mjs +14 -2
  49. package/dist/server/handlers.d.mts +2 -0
  50. package/dist/server/handlers.d.ts +2 -0
  51. package/dist/server/handlers.js +72 -17
  52. package/dist/server/handlers.mjs +3 -2
  53. package/dist/server.d.mts +2 -3
  54. package/dist/server.d.ts +2 -3
  55. package/dist/server.js +151 -89
  56. package/dist/server.mjs +7 -6
  57. package/dist/service.d.mts +1 -2
  58. package/dist/service.d.ts +1 -2
  59. package/dist/service.js +89 -68
  60. package/dist/service.mjs +2 -1
  61. package/dist/{signIn-CEMdUAwd.d.mts → signIn-Cd0P4y9d.d.mts} +9 -1
  62. package/dist/{signIn-VRNzlNyG.d.ts → signIn-DKakyzeu.d.ts} +9 -1
  63. package/package.json +3 -2
  64. package/dist/chunk-5WFR6Y33.mjs +0 -59
package/dist/express.mjs CHANGED
@@ -1,18 +1,19 @@
1
1
  import {
2
2
  DEFAULT_REFRESH_COOKIE,
3
3
  iqAuthMiddleware
4
- } from "./chunk-ZESHDJDU.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-JQRTY5MY.mjs";
12
+ } from "./chunk-KGEPDXHU.mjs";
10
13
  import {
11
- parsePublishableKey
12
- } from "./chunk-5WFR6Y33.mjs";
13
- import {
14
- IQAuthClient
15
- } from "./chunk-MDUHPQMM.mjs";
14
+ assertPublishableKey
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") {
@@ -66,10 +139,7 @@ function readCookieFromReq(req, name) {
66
139
  return void 0;
67
140
  }
68
141
  function iqAuth(options) {
69
- const parsed = parsePublishableKey(options.publishableKey);
70
- if (!parsed) {
71
- throw new Error("@iqauth/sdk/express: invalid publishable key");
72
- }
142
+ const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/express" });
73
143
  const issuer = (options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
74
144
  const client = new IQAuthClient({
75
145
  baseUrl: issuer,
@@ -86,6 +156,8 @@ function iqAuth(options) {
86
156
  if (mountHelpers && path.startsWith(mount + "/")) return next();
87
157
  return verify(req, res, next);
88
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;
89
161
  const attachHelpers = (app) => {
90
162
  app.post(`${mount}/callback`, async (req, res) => {
91
163
  const body = readBody(req);
@@ -96,6 +168,131 @@ function iqAuth(options) {
96
168
  });
97
169
  applyHandlerResponse(res, hr);
98
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
+ }
99
296
  app.post(`${mount}/refresh`, async (req, res) => {
100
297
  const body = readBody(req);
101
298
  const refreshToken = body.refreshToken || readCookieFromReq(req, refreshCookie);
@@ -104,7 +301,8 @@ function iqAuth(options) {
104
301
  });
105
302
  app.post(`${mount}/signout`, async (req, res) => {
106
303
  const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
107
- const hr = await handleSignout(helperConfig, { accessToken });
304
+ const ssoCookieHeader = req.headers?.cookie;
305
+ const hr = await handleSignout(helperConfig, { accessToken, ssoCookieHeader });
108
306
  applyHandlerResponse(res, hr);
109
307
  });
110
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;
517
545
  }
518
- return this.jwksCache?.keys.get(kid) ?? null;
546
+ await this.refreshJwks();
547
+ if (!this.jwksCache) {
548
+ throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
549
+ }
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,
@@ -1767,21 +1788,61 @@ function b64urlDecode(input) {
1767
1788
  const { Buffer: Buffer2 } = require("buffer");
1768
1789
  return Buffer2.from(normalized, "base64").toString("utf8");
1769
1790
  }
1770
- function parsePublishableKey(raw) {
1771
- if (typeof raw !== "string") return null;
1772
- const m = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
1773
- if (!m) return null;
1791
+ function isValidIssuerUrl(iss) {
1792
+ if (typeof iss !== "string" || iss.length === 0) return false;
1793
+ if (!iss.startsWith("http://") && !iss.startsWith("https://")) return false;
1774
1794
  try {
1775
- const json = JSON.parse(b64urlDecode(m[2]));
1776
- if (!json || typeof json !== "object") return null;
1777
- if (typeof json.iss !== "string" || typeof json.appId !== "string" || typeof json.tenantId !== "string" || typeof json.kid !== "string") {
1778
- return null;
1779
- }
1780
- return { mode: m[1], iss: json.iss, appId: json.appId, tenantId: json.tenantId, kid: json.kid, raw };
1795
+ const u = new URL(iss);
1796
+ if (u.protocol !== "http:" && u.protocol !== "https:") return false;
1797
+ if (!u.hostname) return false;
1798
+ return true;
1781
1799
  } catch {
1782
- return null;
1800
+ return false;
1783
1801
  }
1784
1802
  }
1803
+ function assertPublishableKey(raw, opts) {
1804
+ const ctx = opts?.context ? `${opts.context}: ` : "";
1805
+ if (typeof raw !== "string" || raw.length === 0) {
1806
+ throw new IQAuthError(
1807
+ "CONFIG_INVALID",
1808
+ `${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.`
1809
+ );
1810
+ }
1811
+ const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
1812
+ if (!shapeMatch) {
1813
+ throw new IQAuthError(
1814
+ "CONFIG_INVALID",
1815
+ `${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.`
1816
+ );
1817
+ }
1818
+ let decoded;
1819
+ try {
1820
+ decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
1821
+ } catch {
1822
+ throw new IQAuthError(
1823
+ "CONFIG_INVALID",
1824
+ `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
1825
+ );
1826
+ }
1827
+ if (!isPublishableKeyPayload(decoded)) {
1828
+ throw new IQAuthError(
1829
+ "CONFIG_INVALID",
1830
+ `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
1831
+ );
1832
+ }
1833
+ if (!isValidIssuerUrl(decoded.iss)) {
1834
+ throw new IQAuthError(
1835
+ "CONFIG_INVALID",
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.`
1837
+ );
1838
+ }
1839
+ return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
1840
+ }
1841
+ function isPublishableKeyPayload(value) {
1842
+ if (!value || typeof value !== "object") return false;
1843
+ const v = value;
1844
+ return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
1845
+ }
1785
1846
 
1786
1847
  // src/server/handlers.ts
1787
1848
  var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
@@ -1803,12 +1864,7 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
1803
1864
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
1804
1865
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
1805
1866
  function resolve(config) {
1806
- const parsed = parsePublishableKey(config.publishableKey);
1807
- if (!parsed) {
1808
- throw new Error(
1809
- "@iqauth/sdk: invalid publishable key passed to iqAuth helpers (expected pk_test_\u2026 or pk_live_\u2026)"
1810
- );
1811
- }
1867
+ const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
1812
1868
  const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
1813
1869
  return {
1814
1870
  publishableKey: config.publishableKey,
@@ -1979,6 +2035,15 @@ async function handleSignout(config, input) {
1979
2035
  } catch {
1980
2036
  }
1981
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
+ }
1982
2047
  return {
1983
2048
  status: 200,
1984
2049
  body: { success: true, data: { signedOut: true } },
@@ -2023,8 +2088,7 @@ function readCookie(req, name) {
2023
2088
  return void 0;
2024
2089
  }
2025
2090
  async function iqAuth(fastify, options) {
2026
- const parsed = parsePublishableKey(options.publishableKey);
2027
- if (!parsed) throw new Error("@iqauth/sdk/fastify: invalid publishable key");
2091
+ const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/fastify" });
2028
2092
  const issuer = (options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
2029
2093
  const helperConfig = { ...options, issuer };
2030
2094
  const client = new IQAuthClient({
@@ -2086,7 +2150,8 @@ async function iqAuth(fastify, options) {
2086
2150
  fastify.post(`${mount}/signout`, async (req, reply) => {
2087
2151
  const auth = req.headers?.authorization;
2088
2152
  const accessToken = (typeof auth === "string" ? auth.replace(/^Bearer /i, "") : void 0) || readCookie(req, accessCookie);
2089
- 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 }));
2090
2155
  });
2091
2156
  }
2092
2157
  fastify.decorate("iqauth", { client, issuer });