@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.
Files changed (110) hide show
  1. package/README.md +173 -1
  2. package/dist/browser-session.d.mts +4 -4
  3. package/dist/browser-session.d.ts +4 -4
  4. package/dist/browser-session.js +181 -41
  5. package/dist/browser-session.mjs +3 -3
  6. package/dist/browser.d.mts +5 -5
  7. package/dist/browser.d.ts +5 -5
  8. package/dist/browser.js +271 -32
  9. package/dist/browser.mjs +5 -5
  10. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  11. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  12. package/dist/chunk-GLXSIGVS.mjs +66 -0
  13. package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
  14. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  15. package/dist/{chunk-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
  16. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  17. package/dist/chunk-PMAFENVI.mjs +229 -0
  18. package/dist/chunk-RR2MGPTK.mjs +2724 -0
  19. package/dist/{chunk-XAWYUPMO.mjs → chunk-RTJAIBXY.mjs} +220 -20
  20. package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
  21. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  22. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  23. package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
  24. package/dist/cli/index.js +2 -2
  25. package/dist/cli/index.mjs +2 -2
  26. package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
  27. package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
  28. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  29. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  30. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  31. package/dist/{express-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
  32. package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
  33. package/dist/express.d.mts +7 -6
  34. package/dist/express.d.ts +7 -6
  35. package/dist/express.js +349 -52
  36. package/dist/express.mjs +39 -12
  37. package/dist/fastify.d.mts +2 -0
  38. package/dist/fastify.d.ts +2 -0
  39. package/dist/fastify.js +332 -52
  40. package/dist/fastify.mjs +23 -8
  41. package/dist/hono.d.mts +2 -0
  42. package/dist/hono.d.ts +2 -0
  43. package/dist/hono.js +329 -52
  44. package/dist/hono.mjs +20 -8
  45. package/dist/index-5KSZEnDe.d.ts +1626 -0
  46. package/dist/index-CKoZHAoc.d.mts +1626 -0
  47. package/dist/index.d.mts +56 -8
  48. package/dist/index.d.ts +56 -8
  49. package/dist/index.js +565 -69
  50. package/dist/index.mjs +29 -9
  51. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  52. package/dist/locales.d.mts +1 -1
  53. package/dist/locales.d.ts +1 -1
  54. package/dist/mobile.d.mts +77 -7
  55. package/dist/mobile.d.ts +77 -7
  56. package/dist/mobile.js +276 -41
  57. package/dist/mobile.mjs +98 -3
  58. package/dist/next.d.mts +2 -1
  59. package/dist/next.d.ts +2 -1
  60. package/dist/next.js +391 -201
  61. package/dist/next.mjs +22 -7
  62. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
  63. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
  64. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  65. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  66. package/dist/react-permissions.d.mts +52 -0
  67. package/dist/react-permissions.d.ts +52 -0
  68. package/dist/react-permissions.js +239 -0
  69. package/dist/react-permissions.mjs +97 -0
  70. package/dist/react.d.mts +9 -1624
  71. package/dist/react.d.ts +9 -1624
  72. package/dist/react.js +313 -33
  73. package/dist/react.mjs +58 -2632
  74. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  75. package/dist/server/handlers.d.mts +148 -3
  76. package/dist/server/handlers.d.ts +148 -3
  77. package/dist/server/handlers.js +410 -11
  78. package/dist/server/handlers.mjs +12 -3
  79. package/dist/server.d.mts +151 -8
  80. package/dist/server.d.ts +151 -8
  81. package/dist/server.js +406 -50
  82. package/dist/server.mjs +93 -11
  83. package/dist/service.d.mts +4 -4
  84. package/dist/service.d.ts +4 -4
  85. package/dist/service.js +181 -41
  86. package/dist/service.mjs +3 -3
  87. package/dist/{signIn-OCr88Zf8.d.ts → signIn-BLFnz8SV.d.ts} +78 -3
  88. package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
  89. package/dist/{signIn-CiIBTJIh.d.mts → signIn-T-CZ6t6r.d.mts} +78 -3
  90. package/dist/test.mjs +3 -3
  91. package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
  92. package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
  93. package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
  94. package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
  95. package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
  96. package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
  97. package/dist/webhooks.d.mts +100 -17
  98. package/dist/webhooks.d.ts +100 -17
  99. package/dist/webhooks.js +164 -15
  100. package/dist/webhooks.mjs +7 -1
  101. package/dist/ws.d.mts +2 -2
  102. package/dist/ws.d.ts +2 -2
  103. package/dist/ws.js +80 -30
  104. package/dist/ws.mjs +4 -4
  105. package/docs/error-handling.md +101 -0
  106. package/docs/guides/effective-permissions.md +171 -0
  107. package/package.json +13 -3
  108. package/dist/chunk-UKZLOHZG.mjs +0 -83
  109. package/dist/errors-CDdl24MP.d.mts +0 -52
  110. package/dist/errors-CDdl24MP.d.ts +0 -52
package/dist/express.js CHANGED
@@ -38,13 +38,30 @@ __export(express_exports, {
38
38
  module.exports = __toCommonJS(express_exports);
39
39
 
40
40
  // src/errors.ts
41
- var IQAuthError = class extends Error {
42
- constructor(code, message, status, raw) {
41
+ var IQAuthError = class _IQAuthError extends Error {
42
+ constructor(code, message, status, cause) {
43
43
  super(message);
44
44
  this.name = "IQAuthError";
45
45
  this.code = code;
46
46
  this.status = status;
47
- this.raw = raw;
47
+ this.cause = cause;
48
+ this.raw = cause;
49
+ }
50
+ /**
51
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
52
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
53
+ */
54
+ static isIQAuthError(value) {
55
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
56
+ }
57
+ /**
58
+ * Type-narrowed code check. Lets callers write
59
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
60
+ * taxonomy without losing the ability to handle server codes via
61
+ * `err.code === "TOKEN_REVOKED"`.
62
+ */
63
+ is(code) {
64
+ return this.code === code;
48
65
  }
49
66
  };
50
67
  var ErrorCodes = {
@@ -195,7 +212,7 @@ var HttpClient = class {
195
212
  headers: this.buildHeaders(),
196
213
  ...this.isBrowserSession() ? { credentials: "include" } : (() => {
197
214
  const refreshToken = this.config.getRefreshToken();
198
- if (!refreshToken) throw new IQAuthError("TOKEN_INVALID", "No refresh token available");
215
+ if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
199
216
  return { body: JSON.stringify({ refreshToken }) };
200
217
  })()
201
218
  });
@@ -212,7 +229,7 @@ var HttpClient = class {
212
229
  return;
213
230
  }
214
231
  if (!body.data.accessToken || !body.data.refreshToken) {
215
- throw new IQAuthError("TOKEN_INVALID", "Refresh response did not include a token pair");
232
+ throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
216
233
  }
217
234
  const tokens = {
218
235
  accessToken: body.data.accessToken,
@@ -230,7 +247,7 @@ var HttpClient = class {
230
247
  return this.requestWithRetry(method, path, body, options, false);
231
248
  }
232
249
  async requestWithRetry(method, path, body, options, hasRetried) {
233
- if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
250
+ if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
234
251
  await this.attemptRefresh();
235
252
  }
236
253
  const url = `${this.config.baseUrl}${path}`;
@@ -458,6 +475,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
458
475
  "iqvalidate"
459
476
  ];
460
477
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
478
+ function classifyJoseError(err) {
479
+ if (err instanceof import_jose.errors.JWTExpired) {
480
+ return { code: "token_expired", message: "Token has expired" };
481
+ }
482
+ if (err instanceof import_jose.errors.JOSEError) {
483
+ return { code: "token_invalid", message: err.message };
484
+ }
485
+ if (err instanceof Error) {
486
+ return { code: "token_invalid", message: err.message };
487
+ }
488
+ return { code: "token_invalid", message: "Token verification failed" };
489
+ }
461
490
  function decodeProtectedHeader(token) {
462
491
  const parts = token.split(".");
463
492
  if (parts.length < 2) return null;
@@ -494,11 +523,11 @@ var TokensModule = class {
494
523
  async verify(token, options = {}) {
495
524
  const header = decodeProtectedHeader(token);
496
525
  if (!header) {
497
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
526
+ throw new IQAuthError("token_invalid", "Unable to decode token");
498
527
  }
499
528
  const kid = header.kid;
500
529
  if (!kid) {
501
- throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
530
+ throw new IQAuthError("token_invalid", "Token missing kid header");
502
531
  }
503
532
  let cache = await this.ensureCache();
504
533
  if (!cache.byKid.has(kid)) {
@@ -506,7 +535,7 @@ var TokensModule = class {
506
535
  cache = await this.ensureCache();
507
536
  }
508
537
  if (!cache.byKid.has(kid)) {
509
- throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
538
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
510
539
  }
511
540
  const issuer = options.issuer ?? this.defaultIssuer;
512
541
  const audience = options.audience ?? this.defaultAudience;
@@ -522,16 +551,8 @@ var TokensModule = class {
522
551
  const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
523
552
  return payload;
524
553
  } 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
- }
531
- if (err instanceof Error) {
532
- throw new IQAuthError("TOKEN_INVALID", err.message);
533
- }
534
- throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
554
+ const classified = classifyJoseError(err);
555
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
535
556
  }
536
557
  }
537
558
  /**
@@ -573,7 +594,7 @@ var TokensModule = class {
573
594
  getClaims(token) {
574
595
  const claims = this.decode(token);
575
596
  if (!claims) {
576
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
597
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
577
598
  }
578
599
  return claims;
579
600
  }
@@ -583,7 +604,7 @@ var TokensModule = class {
583
604
  }
584
605
  await this.refreshJwks();
585
606
  if (!this.jwksCache) {
586
- throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
607
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
587
608
  }
588
609
  return this.jwksCache;
589
610
  }
@@ -593,22 +614,38 @@ var TokensModule = class {
593
614
  }
594
615
  this.inFlightRefresh = (async () => {
595
616
  try {
596
- const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
617
+ let res;
618
+ try {
619
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
620
+ } catch (err) {
621
+ throw new IQAuthError(
622
+ "network",
623
+ err instanceof Error ? err.message : "JWKS fetch network error",
624
+ void 0,
625
+ err
626
+ );
627
+ }
597
628
  if (!res.ok) {
598
629
  throw new IQAuthError(
599
- "INTERNAL_ERROR",
600
- `Failed to fetch JWKS: ${res.status}`
630
+ "jwks_fetch_failed",
631
+ `Failed to fetch JWKS: ${res.status}`,
632
+ res.status
601
633
  );
602
634
  }
603
635
  let jwks;
604
636
  try {
605
637
  jwks = await res.json();
606
- } catch {
607
- throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
638
+ } catch (err) {
639
+ throw new IQAuthError(
640
+ "jwks_fetch_failed",
641
+ "Malformed JWKS response: invalid JSON",
642
+ res.status,
643
+ err
644
+ );
608
645
  }
609
646
  if (!jwks || !Array.isArray(jwks.keys)) {
610
647
  throw new IQAuthError(
611
- "INTERNAL_ERROR",
648
+ "jwks_fetch_failed",
612
649
  "Malformed JWKS response: expected { keys: [...] }"
613
650
  );
614
651
  }
@@ -616,7 +653,7 @@ var TokensModule = class {
616
653
  for (const key of jwks.keys) {
617
654
  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")) {
618
655
  throw new IQAuthError(
619
- "INTERNAL_ERROR",
656
+ "jwks_fetch_failed",
620
657
  "Malformed JWKS response: key missing required fields"
621
658
  );
622
659
  }
@@ -634,6 +671,19 @@ var TokensModule = class {
634
671
  clearCache() {
635
672
  this.jwksCache = null;
636
673
  }
674
+ /**
675
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
676
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
677
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
678
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
679
+ */
680
+ async prewarm() {
681
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
682
+ try {
683
+ await this.refreshJwks();
684
+ } catch {
685
+ }
686
+ }
637
687
  };
638
688
 
639
689
  // src/modules/sessions.ts
@@ -957,14 +1007,14 @@ var OidcModule = class {
957
1007
  */
958
1008
  async handleCallback(params) {
959
1009
  if (!params.state) {
960
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing state parameter");
1010
+ throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
961
1011
  }
962
1012
  if (!params.code) {
963
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing code parameter");
1013
+ throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
964
1014
  }
965
1015
  const stored = await this.stateStore.get(params.state);
966
1016
  if (!stored) {
967
- throw new IQAuthError("VALIDATION_ERROR", "Unknown or expired OIDC state");
1017
+ throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
968
1018
  }
969
1019
  let tokens;
970
1020
  try {
@@ -982,7 +1032,7 @@ var OidcModule = class {
982
1032
  if (tokens.id_token) {
983
1033
  if (!this.tokensModule) {
984
1034
  throw new IQAuthError(
985
- "INTERNAL_ERROR",
1035
+ "config_invalid",
986
1036
  "OIDC handleCallback received an id_token but no TokensModule is configured for verification"
987
1037
  );
988
1038
  }
@@ -993,7 +1043,7 @@ var OidcModule = class {
993
1043
  const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
994
1044
  if (!tokenNonce || tokenNonce !== stored.nonce) {
995
1045
  throw new IQAuthError(
996
- "TOKEN_INVALID",
1046
+ "token_invalid",
997
1047
  "OIDC id_token nonce did not match the stored value"
998
1048
  );
999
1049
  }
@@ -1194,6 +1244,9 @@ var AppsModule = class {
1194
1244
  * @remarks Wraps GET /api/v1/apps/:appKey
1195
1245
  */
1196
1246
  async get(appKey) {
1247
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1248
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1249
+ }
1197
1250
  return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
1198
1251
  }
1199
1252
  /**
@@ -1213,6 +1266,16 @@ var AppsModule = class {
1213
1266
  401
1214
1267
  );
1215
1268
  }
1269
+ if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
1270
+ throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
1271
+ }
1272
+ if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
1273
+ throw new IQAuthError(
1274
+ "ENVIRONMENT_REQUIRED",
1275
+ "manifest.environment is required and must be 'production', 'staging', or 'development'. This guards against a dev workstation silently overwriting a production app's permission tree.",
1276
+ 400
1277
+ );
1278
+ }
1216
1279
  return this.http.request("POST", "/api/v1/apps/sync", manifest);
1217
1280
  }
1218
1281
  /**
@@ -1222,11 +1285,14 @@ var AppsModule = class {
1222
1285
  * @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
1223
1286
  */
1224
1287
  async isRegistered(appKey) {
1288
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1289
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1290
+ }
1225
1291
  try {
1226
1292
  await this.get(appKey);
1227
1293
  return true;
1228
1294
  } catch (err) {
1229
- if (err.code === "NOT_FOUND" || err.status === 404) {
1295
+ if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
1230
1296
  return false;
1231
1297
  }
1232
1298
  throw err;
@@ -1263,6 +1329,20 @@ var RolesModule = class {
1263
1329
  };
1264
1330
 
1265
1331
  // src/modules/permissionGroups.ts
1332
+ function assertAppKey(appKey, callsite) {
1333
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1334
+ throw new IQAuthError(
1335
+ "VALIDATION_ERROR",
1336
+ `appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
1337
+ 400
1338
+ );
1339
+ }
1340
+ }
1341
+ function assertNodeKey(nodeKey, callsite) {
1342
+ if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
1343
+ throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
1344
+ }
1345
+ }
1266
1346
  var PermissionGroupsModule = class {
1267
1347
  constructor(http) {
1268
1348
  this.http = http;
@@ -1283,7 +1363,14 @@ var PermissionGroupsModule = class {
1283
1363
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
1284
1364
  }
1285
1365
  async addPermission(tenantId, groupId, data) {
1286
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, data);
1366
+ assertAppKey(data?.appKey, "permissionGroups.addPermission");
1367
+ assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
1368
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
1369
+ appKey: data.appKey,
1370
+ nodeKey: data.nodeKey,
1371
+ effect: data.effect,
1372
+ weight: data.weight
1373
+ });
1287
1374
  }
1288
1375
  async removePermission(tenantId, groupId, permissionId) {
1289
1376
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
@@ -1307,21 +1394,51 @@ var PermissionGroupsModule = class {
1307
1394
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
1308
1395
  }
1309
1396
  async addUserOverride(tenantId, userId, data) {
1310
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, data);
1397
+ assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
1398
+ assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
1399
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
1400
+ appKey: data.appKey,
1401
+ nodeKey: data.nodeKey,
1402
+ effect: data.effect,
1403
+ weight: data.weight,
1404
+ expiresAt: data.expiresAt
1405
+ });
1311
1406
  }
1312
1407
  async removeUserOverride(tenantId, userId, overrideId) {
1313
1408
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
1314
1409
  }
1410
+ /**
1411
+ * Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
1412
+ * longer accepted at the SDK boundary; pass it as `appKey` instead. The
1413
+ * server still accepts `product=` from raw HTTP callers during the
1414
+ * deprecation window, but the SDK will not silently translate it.
1415
+ */
1315
1416
  async getEffectivePermissions(tenantId, userId, params) {
1316
- const query = new URLSearchParams();
1317
- if (params.product) query.set("product", params.product);
1318
- if (params.appKey) query.set("appKey", params.appKey);
1319
- const qs = query.toString();
1320
- return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
1417
+ assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
1418
+ const qs = new URLSearchParams({ appKey: params.appKey }).toString();
1419
+ return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
1321
1420
  }
1322
1421
  async checkPermission(tenantId, userId, appKey, nodeKey) {
1422
+ assertAppKey(appKey, "permissionGroups.checkPermission");
1423
+ assertNodeKey(nodeKey, "permissionGroups.checkPermission");
1323
1424
  return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
1324
1425
  }
1426
+ /**
1427
+ * Task #130 — every entry in `checks` must include a non-empty `appKey`
1428
+ * AND `nodeKey`. The SDK validates the whole batch before sending so a
1429
+ * single misconfigured entry can't slip through and silently report
1430
+ * `allowed: false` from the server's per-entry validation branch.
1431
+ */
1432
+ async batchCheckPermissions(tenantId, userId, checks) {
1433
+ if (!Array.isArray(checks) || checks.length === 0) {
1434
+ throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
1435
+ }
1436
+ checks.forEach((c, i) => {
1437
+ assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
1438
+ assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
1439
+ });
1440
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
1441
+ }
1325
1442
  };
1326
1443
 
1327
1444
  // src/modules/apiKeys.ts
@@ -1746,6 +1863,10 @@ var IQAuthClient = class _IQAuthClient {
1746
1863
  this._refreshToken = tokens.refreshToken;
1747
1864
  },
1748
1865
  autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
1866
+ // `'app-state'` is mobile-only — on any other environment we treat it
1867
+ // as the default `true` (proactive refresh ON). Only the mobile client
1868
+ // disables proactive refresh and replaces it with an AppState listener.
1869
+ proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
1749
1870
  onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
1750
1871
  sessionHeaderName: config.sessionHeaderName,
1751
1872
  sessionHeaderValue: config.sessionHeaderValue,
@@ -1786,6 +1907,13 @@ var IQAuthClient = class _IQAuthClient {
1786
1907
  static forServer(config) {
1787
1908
  return new _IQAuthClient({ ...config, environment: "server" });
1788
1909
  }
1910
+ /**
1911
+ * Construct a mobile-environment client. NOTE: this constructor does NOT
1912
+ * subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
1913
+ * is passed — it only disables the per-request proactive refresh. Use
1914
+ * `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
1915
+ * AppState-driven refresh behavior (recommended for Expo / React Native).
1916
+ */
1789
1917
  static forMobile(config) {
1790
1918
  return new _IQAuthClient({ ...config, environment: "mobile" });
1791
1919
  }
@@ -1802,6 +1930,18 @@ var IQAuthClient = class _IQAuthClient {
1802
1930
  getRefreshToken() {
1803
1931
  return this._refreshToken;
1804
1932
  }
1933
+ /**
1934
+ * Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
1935
+ * refresh round-trip on the request hot path doesn't pay the discovery
1936
+ * fetch latency. Safe to call repeatedly. Errors are swallowed; callers
1937
+ * may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
1938
+ */
1939
+ async prewarm() {
1940
+ await Promise.all([
1941
+ this.tokens.prewarm(),
1942
+ this.oidc.getDiscovery().catch(() => void 0)
1943
+ ]);
1944
+ }
1805
1945
  getCurrentClaims() {
1806
1946
  if (!this._accessToken) return null;
1807
1947
  return this.tokens.decode(this._accessToken);
@@ -1842,14 +1982,14 @@ function assertPublishableKey(raw, opts) {
1842
1982
  const ctx = opts?.context ? `${opts.context}: ` : "";
1843
1983
  if (typeof raw !== "string" || raw.length === 0) {
1844
1984
  throw new IQAuthError(
1845
- "CONFIG_INVALID",
1985
+ "config_invalid",
1846
1986
  `${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.`
1847
1987
  );
1848
1988
  }
1849
1989
  const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
1850
1990
  if (!shapeMatch) {
1851
1991
  throw new IQAuthError(
1852
- "CONFIG_INVALID",
1992
+ "config_invalid",
1853
1993
  `${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.`
1854
1994
  );
1855
1995
  }
@@ -1858,19 +1998,19 @@ function assertPublishableKey(raw, opts) {
1858
1998
  decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
1859
1999
  } catch {
1860
2000
  throw new IQAuthError(
1861
- "CONFIG_INVALID",
2001
+ "config_invalid",
1862
2002
  `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
1863
2003
  );
1864
2004
  }
1865
2005
  if (!isPublishableKeyPayload(decoded)) {
1866
2006
  throw new IQAuthError(
1867
- "CONFIG_INVALID",
2007
+ "config_invalid",
1868
2008
  `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
1869
2009
  );
1870
2010
  }
1871
2011
  if (!isValidIssuerUrl(decoded.iss)) {
1872
2012
  throw new IQAuthError(
1873
- "CONFIG_INVALID",
2013
+ "config_invalid",
1874
2014
  `${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.`
1875
2015
  );
1876
2016
  }
@@ -1884,12 +2024,18 @@ function isPublishableKeyPayload(value) {
1884
2024
 
1885
2025
  // src/middleware/express.ts
1886
2026
  var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
2027
+ // Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
1887
2028
  "TOKEN_INVALID",
1888
2029
  "TOKEN_EXPIRED",
1889
2030
  "TOKEN_REVOKED",
1890
2031
  "SESSION_EXPIRED",
1891
2032
  "SESSION_INVALID",
1892
- "AUTH_REQUIRED"
2033
+ "AUTH_REQUIRED",
2034
+ // Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
2035
+ // Mapped to 401 here so framework consumers don't have to learn the new
2036
+ // codes to keep their auth-failure handling working.
2037
+ "token_invalid",
2038
+ "token_expired"
1893
2039
  ]);
1894
2040
  var DEFAULT_ACCESS_COOKIE = "iqauth_at";
1895
2041
  var DEFAULT_REFRESH_COOKIE = "iqauth_rt";
@@ -2073,6 +2219,41 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
2073
2219
  }
2074
2220
 
2075
2221
  // src/server/handlers.ts
2222
+ async function buildUserinfoResponse(claims, opts = {}) {
2223
+ const baseUser = {
2224
+ sub: claims.sub,
2225
+ email: claims.email,
2226
+ name: claims.name,
2227
+ tenantId: claims.tenantId,
2228
+ vendorId: claims.vendorId,
2229
+ roles: claims.roles ?? [],
2230
+ entitlements: claims.entitlements ?? []
2231
+ };
2232
+ const enriched = opts.enrich ? await opts.enrich(claims) : null;
2233
+ const user = enriched ? { ...baseUser, ...enriched } : baseUser;
2234
+ return {
2235
+ success: true,
2236
+ data: {
2237
+ user,
2238
+ claims,
2239
+ tenantId: claims.tenantId ?? null
2240
+ }
2241
+ };
2242
+ }
2243
+ function emitTiming(cfg, event) {
2244
+ if (cfg.debug) {
2245
+ try {
2246
+ console.debug("[iqauth_helper]", event);
2247
+ } catch {
2248
+ }
2249
+ }
2250
+ if (cfg.onTimingEvent) {
2251
+ try {
2252
+ cfg.onTimingEvent(event);
2253
+ } catch {
2254
+ }
2255
+ }
2256
+ }
2076
2257
  var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
2077
2258
  "TOKEN_REVOKED",
2078
2259
  "SESSION_REVOKED",
@@ -2112,7 +2293,11 @@ function resolve(config) {
2112
2293
  })),
2113
2294
  appId: parsed.appId,
2114
2295
  tenantId: parsed.tenantId,
2115
- clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
2296
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
2297
+ debug: config.debug,
2298
+ onTimingEvent: config.onTimingEvent,
2299
+ signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
2300
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
2116
2301
  };
2117
2302
  }
2118
2303
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -2129,13 +2314,40 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
2129
2314
  }
2130
2315
  function clearCookies(cfg) {
2131
2316
  return [
2132
- makeCookie(cfg, cfg.accessCookieName, "", 0),
2133
- makeCookie(cfg, cfg.refreshCookieName, "", 0)
2317
+ { ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
2318
+ { ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
2134
2319
  ];
2135
2320
  }
2321
+ var DEFAULT_SIGNOUT_TTL_MS = 6e4;
2322
+ var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
2323
+ function pruneInMemoryMarkers(now) {
2324
+ if (inMemorySignoutMarkers.size === 0) return;
2325
+ for (const [k, exp] of inMemorySignoutMarkers) {
2326
+ if (exp <= now) inMemorySignoutMarkers.delete(k);
2327
+ }
2328
+ }
2329
+ var defaultSignoutRegistry = {
2330
+ mark(token, ttlMs) {
2331
+ const now = Date.now();
2332
+ pruneInMemoryMarkers(now);
2333
+ inMemorySignoutMarkers.set(token, now + ttlMs);
2334
+ },
2335
+ has(token) {
2336
+ const now = Date.now();
2337
+ const exp = inMemorySignoutMarkers.get(token);
2338
+ if (!exp) return false;
2339
+ if (exp <= now) {
2340
+ inMemorySignoutMarkers.delete(token);
2341
+ return false;
2342
+ }
2343
+ return true;
2344
+ }
2345
+ };
2136
2346
  async function handleCallback(config, input) {
2137
2347
  const cfg = resolve(config);
2348
+ const t0 = Date.now();
2138
2349
  if (!input.code || !input.redirectUri) {
2350
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
2139
2351
  return {
2140
2352
  status: 400,
2141
2353
  body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
@@ -2143,6 +2355,7 @@ async function handleCallback(config, input) {
2143
2355
  };
2144
2356
  }
2145
2357
  if (!cfg.secretKey) {
2358
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
2146
2359
  return {
2147
2360
  status: 500,
2148
2361
  body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
@@ -2166,6 +2379,7 @@ async function handleCallback(config, input) {
2166
2379
  });
2167
2380
  const json = await res.json().catch(() => ({}));
2168
2381
  if (!res.ok || !json.access_token) {
2382
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
2169
2383
  return {
2170
2384
  status: res.status || 502,
2171
2385
  body: {
@@ -2185,6 +2399,7 @@ async function handleCallback(config, input) {
2185
2399
  if (json.refresh_token) {
2186
2400
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
2187
2401
  }
2402
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
2188
2403
  return {
2189
2404
  status: 200,
2190
2405
  body: { success: true, data: { authenticated: true } },
@@ -2193,8 +2408,18 @@ async function handleCallback(config, input) {
2193
2408
  }
2194
2409
  async function handleRefresh(config, input) {
2195
2410
  const cfg = resolve(config);
2411
+ const t0 = Date.now();
2196
2412
  const refreshToken = input.refreshToken;
2413
+ const idemKey = input.idempotencyToken;
2414
+ if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
2415
+ return {
2416
+ status: 401,
2417
+ body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
2418
+ cookies: clearCookies(cfg)
2419
+ };
2420
+ }
2197
2421
  if (!refreshToken) {
2422
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
2198
2423
  return {
2199
2424
  status: 401,
2200
2425
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
@@ -2210,6 +2435,7 @@ async function handleRefresh(config, input) {
2210
2435
  if (!res.ok || !json.success || !json.data?.accessToken) {
2211
2436
  const status = res.status || 401;
2212
2437
  const errorCode = json.error?.code || "TOKEN_INVALID";
2438
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
2213
2439
  const shouldClear = shouldClearCookiesOnFailure(
2214
2440
  cfg.clearCookiesOnRefreshFailure,
2215
2441
  status,
@@ -2233,6 +2459,7 @@ async function handleRefresh(config, input) {
2233
2459
  if (json.data.refreshToken) {
2234
2460
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
2235
2461
  }
2462
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
2236
2463
  return {
2237
2464
  status: 200,
2238
2465
  body: { success: true, data: { accessToken: json.data.accessToken } },
@@ -2241,6 +2468,10 @@ async function handleRefresh(config, input) {
2241
2468
  }
2242
2469
  async function handleSignout(config, input) {
2243
2470
  const cfg = resolve(config);
2471
+ const t0 = Date.now();
2472
+ if (input.idempotencyToken) {
2473
+ await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
2474
+ }
2244
2475
  if (input.accessToken) {
2245
2476
  try {
2246
2477
  await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
@@ -2262,15 +2493,56 @@ async function handleSignout(config, input) {
2262
2493
  } catch {
2263
2494
  }
2264
2495
  }
2496
+ emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
2265
2497
  return {
2266
2498
  status: 200,
2267
2499
  body: { success: true, data: { signedOut: true } },
2268
2500
  cookies: clearCookies(cfg)
2269
2501
  };
2270
2502
  }
2503
+ var TOKENS_CACHE = /* @__PURE__ */ new Map();
2504
+ function getTokensFor(issuer) {
2505
+ let m = TOKENS_CACHE.get(issuer);
2506
+ if (!m) {
2507
+ m = new TokensModule(issuer);
2508
+ TOKENS_CACHE.set(issuer, m);
2509
+ }
2510
+ return m;
2511
+ }
2512
+ async function handleUserinfo(config, input) {
2513
+ const cfg = resolve(config);
2514
+ if (!input.accessToken) {
2515
+ return {
2516
+ status: 401,
2517
+ body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
2518
+ cookies: []
2519
+ };
2520
+ }
2521
+ let claims;
2522
+ try {
2523
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
2524
+ } catch (err) {
2525
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2526
+ const message = err instanceof Error ? err.message : "Access token verification failed";
2527
+ return {
2528
+ status: 401,
2529
+ body: { success: false, error: { code, message } },
2530
+ cookies: []
2531
+ };
2532
+ }
2533
+ const envelope = await buildUserinfoResponse(claims, {
2534
+ enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
2535
+ });
2536
+ return {
2537
+ status: 200,
2538
+ body: envelope,
2539
+ cookies: []
2540
+ };
2541
+ }
2271
2542
 
2272
2543
  // src/express.ts
2273
2544
  var PKCE_COOKIE = "iqauth_pkce";
2545
+ var IDEMPOTENCY_HEADER = "x-iqauth-idempotency";
2274
2546
  function escapeHtml(s) {
2275
2547
  return s.replace(/[&<>"']/g, (c) => {
2276
2548
  switch (c) {
@@ -2344,6 +2616,17 @@ function defaultBrandedSpinner(args) {
2344
2616
  }
2345
2617
  function applyHandlerResponse(res, hr) {
2346
2618
  for (const c of hr.cookies) {
2619
+ if (c.clear && typeof res.clearCookie === "function") {
2620
+ const opts = {
2621
+ httpOnly: c.httpOnly,
2622
+ secure: c.secure,
2623
+ sameSite: c.sameSite,
2624
+ path: c.path
2625
+ };
2626
+ if (c.domain) opts.domain = c.domain;
2627
+ res.clearCookie(c.name, opts);
2628
+ continue;
2629
+ }
2347
2630
  if (typeof res.cookie === "function") {
2348
2631
  const opts = {
2349
2632
  httpOnly: c.httpOnly,
@@ -2353,11 +2636,13 @@ function applyHandlerResponse(res, hr) {
2353
2636
  maxAge: c.maxAge * 1e3
2354
2637
  };
2355
2638
  if (c.domain) opts.domain = c.domain;
2639
+ if (c.clear) opts.expires = /* @__PURE__ */ new Date(0);
2356
2640
  res.cookie(c.name, c.value, opts);
2357
2641
  } else {
2358
2642
  const existing = res.getHeader?.("Set-Cookie") || [];
2359
2643
  const list = Array.isArray(existing) ? existing : [existing];
2360
2644
  const parts = [`${c.name}=${encodeURIComponent(c.value)}`, `Path=${c.path}`, `Max-Age=${c.maxAge}`, `SameSite=${c.sameSite}`];
2645
+ if (c.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
2361
2646
  if (c.secure) parts.push("Secure");
2362
2647
  if (c.httpOnly) parts.push("HttpOnly");
2363
2648
  if (c.domain) parts.push(`Domain=${c.domain}`);
@@ -2408,6 +2693,7 @@ function iqAuth(options) {
2408
2693
  const inline = options.inlineCallback === true ? {} : options.inlineCallback && typeof options.inlineCallback === "object" ? options.inlineCallback : null;
2409
2694
  const inlineBranded = inline?.branded === true ? {} : inline?.branded && typeof inline.branded === "object" ? inline.branded : null;
2410
2695
  const attachHelpers = (app) => {
2696
+ void client.prewarm();
2411
2697
  app.post(`${mount}/callback`, async (req, res) => {
2412
2698
  const body = readBody(req);
2413
2699
  const hr = await handleCallback(helperConfig, {
@@ -2545,13 +2831,23 @@ function iqAuth(options) {
2545
2831
  app.post(`${mount}/refresh`, async (req, res) => {
2546
2832
  const body = readBody(req);
2547
2833
  const refreshToken = body.refreshToken || readCookieFromReq(req, refreshCookie);
2548
- const hr = await handleRefresh(helperConfig, { refreshToken });
2834
+ const idempotencyToken = req.headers?.[IDEMPOTENCY_HEADER] || body.idempotencyToken;
2835
+ const hr = await handleRefresh(helperConfig, { refreshToken, idempotencyToken });
2549
2836
  applyHandlerResponse(res, hr);
2550
2837
  });
2838
+ if (options.mountUserinfo && typeof app.get === "function") {
2839
+ app.get(`${mount}/me`, async (req, res) => {
2840
+ const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
2841
+ const hr = await handleUserinfo(helperConfig, { accessToken, req });
2842
+ applyHandlerResponse(res, hr);
2843
+ });
2844
+ }
2551
2845
  app.post(`${mount}/signout`, async (req, res) => {
2552
2846
  const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
2847
+ const refreshToken = readCookieFromReq(req, refreshCookie);
2553
2848
  const ssoCookieHeader = req.headers?.cookie;
2554
- const hr = await handleSignout(helperConfig, { accessToken, ssoCookieHeader });
2849
+ const idempotencyToken = req.headers?.[IDEMPOTENCY_HEADER];
2850
+ const hr = await handleSignout(helperConfig, { accessToken, refreshToken, idempotencyToken, ssoCookieHeader });
2555
2851
  applyHandlerResponse(res, hr);
2556
2852
  });
2557
2853
  };
@@ -2559,6 +2855,7 @@ function iqAuth(options) {
2559
2855
  composed.middleware = middleware;
2560
2856
  composed.attachHelpers = attachHelpers;
2561
2857
  composed.client = client;
2858
+ composed.prewarm = () => client.prewarm();
2562
2859
  return composed;
2563
2860
  }
2564
2861
  // Annotate the CommonJS export names for ESM import in node: