@iqauth/sdk 2.6.3 → 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 (112) 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 +10 -8
  10. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  11. package/dist/chunk-C2ZTBOAC.mjs +36 -0
  12. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  13. package/dist/chunk-GLXSIGVS.mjs +66 -0
  14. package/dist/{chunk-TKZTCPEK.mjs → chunk-GN37E64I.mjs} +32 -40
  15. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  16. package/dist/{chunk-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
  17. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  18. package/dist/chunk-PMAFENVI.mjs +229 -0
  19. package/dist/chunk-RR2MGPTK.mjs +2724 -0
  20. package/dist/{chunk-76W5TLQQ.mjs → chunk-RTJAIBXY.mjs} +220 -20
  21. package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
  22. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  23. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  24. package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
  25. package/dist/cli/index.js +2 -2
  26. package/dist/cli/index.mjs +2 -2
  27. package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
  28. package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
  29. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  30. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  31. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  32. package/dist/{express-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
  33. package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
  34. package/dist/express.d.mts +7 -6
  35. package/dist/express.d.ts +7 -6
  36. package/dist/express.js +349 -52
  37. package/dist/express.mjs +39 -12
  38. package/dist/fastify.d.mts +2 -0
  39. package/dist/fastify.d.ts +2 -0
  40. package/dist/fastify.js +332 -52
  41. package/dist/fastify.mjs +23 -8
  42. package/dist/hono.d.mts +2 -0
  43. package/dist/hono.d.ts +2 -0
  44. package/dist/hono.js +329 -52
  45. package/dist/hono.mjs +20 -8
  46. package/dist/index-5KSZEnDe.d.ts +1626 -0
  47. package/dist/index-CKoZHAoc.d.mts +1626 -0
  48. package/dist/index.d.mts +56 -8
  49. package/dist/index.d.ts +56 -8
  50. package/dist/index.js +565 -69
  51. package/dist/index.mjs +29 -9
  52. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  53. package/dist/locales.d.mts +1 -1
  54. package/dist/locales.d.ts +1 -1
  55. package/dist/mobile.d.mts +77 -7
  56. package/dist/mobile.d.ts +77 -7
  57. package/dist/mobile.js +276 -41
  58. package/dist/mobile.mjs +98 -3
  59. package/dist/next.d.mts +2 -1
  60. package/dist/next.d.ts +2 -1
  61. package/dist/next.js +391 -201
  62. package/dist/next.mjs +22 -7
  63. package/dist/pkce-7WKV4OIN.mjs +11 -0
  64. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
  65. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
  66. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  67. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  68. package/dist/react-permissions.d.mts +52 -0
  69. package/dist/react-permissions.d.ts +52 -0
  70. package/dist/react-permissions.js +239 -0
  71. package/dist/react-permissions.mjs +97 -0
  72. package/dist/react.d.mts +9 -1624
  73. package/dist/react.d.ts +9 -1624
  74. package/dist/react.js +343 -36
  75. package/dist/react.mjs +59 -2611
  76. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  77. package/dist/server/handlers.d.mts +148 -3
  78. package/dist/server/handlers.d.ts +148 -3
  79. package/dist/server/handlers.js +410 -11
  80. package/dist/server/handlers.mjs +12 -3
  81. package/dist/server.d.mts +151 -8
  82. package/dist/server.d.ts +151 -8
  83. package/dist/server.js +406 -50
  84. package/dist/server.mjs +93 -11
  85. package/dist/service.d.mts +4 -4
  86. package/dist/service.d.ts +4 -4
  87. package/dist/service.js +181 -41
  88. package/dist/service.mjs +3 -3
  89. package/dist/{signIn-CiIBTJIh.d.mts → signIn-BLFnz8SV.d.ts} +78 -3
  90. package/dist/{signIn-CCY4JE5G.mjs → signIn-SHBW6Z4T.mjs} +2 -1
  91. package/dist/{signIn-OCr88Zf8.d.ts → signIn-T-CZ6t6r.d.mts} +78 -3
  92. package/dist/test.mjs +3 -3
  93. package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
  94. package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
  95. package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
  96. package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
  97. package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
  98. package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
  99. package/dist/webhooks.d.mts +100 -17
  100. package/dist/webhooks.d.ts +100 -17
  101. package/dist/webhooks.js +164 -15
  102. package/dist/webhooks.mjs +7 -1
  103. package/dist/ws.d.mts +2 -2
  104. package/dist/ws.d.ts +2 -2
  105. package/dist/ws.js +80 -30
  106. package/dist/ws.mjs +4 -4
  107. package/docs/error-handling.md +101 -0
  108. package/docs/guides/effective-permissions.md +171 -0
  109. package/package.json +13 -3
  110. package/dist/chunk-UKZLOHZG.mjs +0 -83
  111. package/dist/errors-CDdl24MP.d.mts +0 -52
  112. package/dist/errors-CDdl24MP.d.ts +0 -52
package/dist/mobile.js CHANGED
@@ -39,13 +39,30 @@ __export(mobile_exports, {
39
39
  module.exports = __toCommonJS(mobile_exports);
40
40
 
41
41
  // src/errors.ts
42
- var IQAuthError = class extends Error {
43
- constructor(code, message, status, raw) {
42
+ var IQAuthError = class _IQAuthError extends Error {
43
+ constructor(code, message, status, cause) {
44
44
  super(message);
45
45
  this.name = "IQAuthError";
46
46
  this.code = code;
47
47
  this.status = status;
48
- this.raw = raw;
48
+ this.cause = cause;
49
+ this.raw = cause;
50
+ }
51
+ /**
52
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
53
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
54
+ */
55
+ static isIQAuthError(value) {
56
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
57
+ }
58
+ /**
59
+ * Type-narrowed code check. Lets callers write
60
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
61
+ * taxonomy without losing the ability to handle server codes via
62
+ * `err.code === "TOKEN_REVOKED"`.
63
+ */
64
+ is(code) {
65
+ return this.code === code;
49
66
  }
50
67
  };
51
68
  var ErrorCodes = {
@@ -196,7 +213,7 @@ var HttpClient = class {
196
213
  headers: this.buildHeaders(),
197
214
  ...this.isBrowserSession() ? { credentials: "include" } : (() => {
198
215
  const refreshToken = this.config.getRefreshToken();
199
- if (!refreshToken) throw new IQAuthError("TOKEN_INVALID", "No refresh token available");
216
+ if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
200
217
  return { body: JSON.stringify({ refreshToken }) };
201
218
  })()
202
219
  });
@@ -213,7 +230,7 @@ var HttpClient = class {
213
230
  return;
214
231
  }
215
232
  if (!body.data.accessToken || !body.data.refreshToken) {
216
- throw new IQAuthError("TOKEN_INVALID", "Refresh response did not include a token pair");
233
+ throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
217
234
  }
218
235
  const tokens = {
219
236
  accessToken: body.data.accessToken,
@@ -231,7 +248,7 @@ var HttpClient = class {
231
248
  return this.requestWithRetry(method, path, body, options, false);
232
249
  }
233
250
  async requestWithRetry(method, path, body, options, hasRetried) {
234
- if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
251
+ if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
235
252
  await this.attemptRefresh();
236
253
  }
237
254
  const url = `${this.config.baseUrl}${path}`;
@@ -459,6 +476,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
459
476
  "iqvalidate"
460
477
  ];
461
478
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
479
+ function classifyJoseError(err) {
480
+ if (err instanceof import_jose.errors.JWTExpired) {
481
+ return { code: "token_expired", message: "Token has expired" };
482
+ }
483
+ if (err instanceof import_jose.errors.JOSEError) {
484
+ return { code: "token_invalid", message: err.message };
485
+ }
486
+ if (err instanceof Error) {
487
+ return { code: "token_invalid", message: err.message };
488
+ }
489
+ return { code: "token_invalid", message: "Token verification failed" };
490
+ }
462
491
  function decodeProtectedHeader(token) {
463
492
  const parts = token.split(".");
464
493
  if (parts.length < 2) return null;
@@ -495,11 +524,11 @@ var TokensModule = class {
495
524
  async verify(token, options = {}) {
496
525
  const header = decodeProtectedHeader(token);
497
526
  if (!header) {
498
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
527
+ throw new IQAuthError("token_invalid", "Unable to decode token");
499
528
  }
500
529
  const kid = header.kid;
501
530
  if (!kid) {
502
- throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
531
+ throw new IQAuthError("token_invalid", "Token missing kid header");
503
532
  }
504
533
  let cache = await this.ensureCache();
505
534
  if (!cache.byKid.has(kid)) {
@@ -507,7 +536,7 @@ var TokensModule = class {
507
536
  cache = await this.ensureCache();
508
537
  }
509
538
  if (!cache.byKid.has(kid)) {
510
- throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
539
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
511
540
  }
512
541
  const issuer = options.issuer ?? this.defaultIssuer;
513
542
  const audience = options.audience ?? this.defaultAudience;
@@ -523,16 +552,8 @@ var TokensModule = class {
523
552
  const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
524
553
  return payload;
525
554
  } catch (err) {
526
- if (err instanceof import_jose.errors.JWTExpired) {
527
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
528
- }
529
- if (err instanceof import_jose.errors.JOSEError) {
530
- throw new IQAuthError("TOKEN_INVALID", err.message);
531
- }
532
- if (err instanceof Error) {
533
- throw new IQAuthError("TOKEN_INVALID", err.message);
534
- }
535
- throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
555
+ const classified = classifyJoseError(err);
556
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
536
557
  }
537
558
  }
538
559
  /**
@@ -574,7 +595,7 @@ var TokensModule = class {
574
595
  getClaims(token) {
575
596
  const claims = this.decode(token);
576
597
  if (!claims) {
577
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
598
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
578
599
  }
579
600
  return claims;
580
601
  }
@@ -584,7 +605,7 @@ var TokensModule = class {
584
605
  }
585
606
  await this.refreshJwks();
586
607
  if (!this.jwksCache) {
587
- throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
608
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
588
609
  }
589
610
  return this.jwksCache;
590
611
  }
@@ -594,22 +615,38 @@ var TokensModule = class {
594
615
  }
595
616
  this.inFlightRefresh = (async () => {
596
617
  try {
597
- const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
618
+ let res;
619
+ try {
620
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
621
+ } catch (err) {
622
+ throw new IQAuthError(
623
+ "network",
624
+ err instanceof Error ? err.message : "JWKS fetch network error",
625
+ void 0,
626
+ err
627
+ );
628
+ }
598
629
  if (!res.ok) {
599
630
  throw new IQAuthError(
600
- "INTERNAL_ERROR",
601
- `Failed to fetch JWKS: ${res.status}`
631
+ "jwks_fetch_failed",
632
+ `Failed to fetch JWKS: ${res.status}`,
633
+ res.status
602
634
  );
603
635
  }
604
636
  let jwks;
605
637
  try {
606
638
  jwks = await res.json();
607
- } catch {
608
- throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
639
+ } catch (err) {
640
+ throw new IQAuthError(
641
+ "jwks_fetch_failed",
642
+ "Malformed JWKS response: invalid JSON",
643
+ res.status,
644
+ err
645
+ );
609
646
  }
610
647
  if (!jwks || !Array.isArray(jwks.keys)) {
611
648
  throw new IQAuthError(
612
- "INTERNAL_ERROR",
649
+ "jwks_fetch_failed",
613
650
  "Malformed JWKS response: expected { keys: [...] }"
614
651
  );
615
652
  }
@@ -617,7 +654,7 @@ var TokensModule = class {
617
654
  for (const key of jwks.keys) {
618
655
  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")) {
619
656
  throw new IQAuthError(
620
- "INTERNAL_ERROR",
657
+ "jwks_fetch_failed",
621
658
  "Malformed JWKS response: key missing required fields"
622
659
  );
623
660
  }
@@ -635,6 +672,19 @@ var TokensModule = class {
635
672
  clearCache() {
636
673
  this.jwksCache = null;
637
674
  }
675
+ /**
676
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
677
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
678
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
679
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
680
+ */
681
+ async prewarm() {
682
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
683
+ try {
684
+ await this.refreshJwks();
685
+ } catch {
686
+ }
687
+ }
638
688
  };
639
689
 
640
690
  // src/modules/sessions.ts
@@ -958,14 +1008,14 @@ var OidcModule = class {
958
1008
  */
959
1009
  async handleCallback(params) {
960
1010
  if (!params.state) {
961
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing state parameter");
1011
+ throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
962
1012
  }
963
1013
  if (!params.code) {
964
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing code parameter");
1014
+ throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
965
1015
  }
966
1016
  const stored = await this.stateStore.get(params.state);
967
1017
  if (!stored) {
968
- throw new IQAuthError("VALIDATION_ERROR", "Unknown or expired OIDC state");
1018
+ throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
969
1019
  }
970
1020
  let tokens;
971
1021
  try {
@@ -983,7 +1033,7 @@ var OidcModule = class {
983
1033
  if (tokens.id_token) {
984
1034
  if (!this.tokensModule) {
985
1035
  throw new IQAuthError(
986
- "INTERNAL_ERROR",
1036
+ "config_invalid",
987
1037
  "OIDC handleCallback received an id_token but no TokensModule is configured for verification"
988
1038
  );
989
1039
  }
@@ -994,7 +1044,7 @@ var OidcModule = class {
994
1044
  const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
995
1045
  if (!tokenNonce || tokenNonce !== stored.nonce) {
996
1046
  throw new IQAuthError(
997
- "TOKEN_INVALID",
1047
+ "token_invalid",
998
1048
  "OIDC id_token nonce did not match the stored value"
999
1049
  );
1000
1050
  }
@@ -1195,6 +1245,9 @@ var AppsModule = class {
1195
1245
  * @remarks Wraps GET /api/v1/apps/:appKey
1196
1246
  */
1197
1247
  async get(appKey) {
1248
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1249
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1250
+ }
1198
1251
  return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
1199
1252
  }
1200
1253
  /**
@@ -1214,6 +1267,16 @@ var AppsModule = class {
1214
1267
  401
1215
1268
  );
1216
1269
  }
1270
+ if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
1271
+ throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
1272
+ }
1273
+ if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
1274
+ throw new IQAuthError(
1275
+ "ENVIRONMENT_REQUIRED",
1276
+ "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.",
1277
+ 400
1278
+ );
1279
+ }
1217
1280
  return this.http.request("POST", "/api/v1/apps/sync", manifest);
1218
1281
  }
1219
1282
  /**
@@ -1223,11 +1286,14 @@ var AppsModule = class {
1223
1286
  * @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
1224
1287
  */
1225
1288
  async isRegistered(appKey) {
1289
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1290
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1291
+ }
1226
1292
  try {
1227
1293
  await this.get(appKey);
1228
1294
  return true;
1229
1295
  } catch (err) {
1230
- if (err.code === "NOT_FOUND" || err.status === 404) {
1296
+ if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
1231
1297
  return false;
1232
1298
  }
1233
1299
  throw err;
@@ -1264,6 +1330,20 @@ var RolesModule = class {
1264
1330
  };
1265
1331
 
1266
1332
  // src/modules/permissionGroups.ts
1333
+ function assertAppKey(appKey, callsite) {
1334
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1335
+ throw new IQAuthError(
1336
+ "VALIDATION_ERROR",
1337
+ `appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
1338
+ 400
1339
+ );
1340
+ }
1341
+ }
1342
+ function assertNodeKey(nodeKey, callsite) {
1343
+ if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
1344
+ throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
1345
+ }
1346
+ }
1267
1347
  var PermissionGroupsModule = class {
1268
1348
  constructor(http) {
1269
1349
  this.http = http;
@@ -1284,7 +1364,14 @@ var PermissionGroupsModule = class {
1284
1364
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
1285
1365
  }
1286
1366
  async addPermission(tenantId, groupId, data) {
1287
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, data);
1367
+ assertAppKey(data?.appKey, "permissionGroups.addPermission");
1368
+ assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
1369
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
1370
+ appKey: data.appKey,
1371
+ nodeKey: data.nodeKey,
1372
+ effect: data.effect,
1373
+ weight: data.weight
1374
+ });
1288
1375
  }
1289
1376
  async removePermission(tenantId, groupId, permissionId) {
1290
1377
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
@@ -1308,21 +1395,51 @@ var PermissionGroupsModule = class {
1308
1395
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
1309
1396
  }
1310
1397
  async addUserOverride(tenantId, userId, data) {
1311
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, data);
1398
+ assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
1399
+ assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
1400
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
1401
+ appKey: data.appKey,
1402
+ nodeKey: data.nodeKey,
1403
+ effect: data.effect,
1404
+ weight: data.weight,
1405
+ expiresAt: data.expiresAt
1406
+ });
1312
1407
  }
1313
1408
  async removeUserOverride(tenantId, userId, overrideId) {
1314
1409
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
1315
1410
  }
1411
+ /**
1412
+ * Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
1413
+ * longer accepted at the SDK boundary; pass it as `appKey` instead. The
1414
+ * server still accepts `product=` from raw HTTP callers during the
1415
+ * deprecation window, but the SDK will not silently translate it.
1416
+ */
1316
1417
  async getEffectivePermissions(tenantId, userId, params) {
1317
- const query = new URLSearchParams();
1318
- if (params.product) query.set("product", params.product);
1319
- if (params.appKey) query.set("appKey", params.appKey);
1320
- const qs = query.toString();
1321
- return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
1418
+ assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
1419
+ const qs = new URLSearchParams({ appKey: params.appKey }).toString();
1420
+ return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
1322
1421
  }
1323
1422
  async checkPermission(tenantId, userId, appKey, nodeKey) {
1423
+ assertAppKey(appKey, "permissionGroups.checkPermission");
1424
+ assertNodeKey(nodeKey, "permissionGroups.checkPermission");
1324
1425
  return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
1325
1426
  }
1427
+ /**
1428
+ * Task #130 — every entry in `checks` must include a non-empty `appKey`
1429
+ * AND `nodeKey`. The SDK validates the whole batch before sending so a
1430
+ * single misconfigured entry can't slip through and silently report
1431
+ * `allowed: false` from the server's per-entry validation branch.
1432
+ */
1433
+ async batchCheckPermissions(tenantId, userId, checks) {
1434
+ if (!Array.isArray(checks) || checks.length === 0) {
1435
+ throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
1436
+ }
1437
+ checks.forEach((c, i) => {
1438
+ assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
1439
+ assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
1440
+ });
1441
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
1442
+ }
1326
1443
  };
1327
1444
 
1328
1445
  // src/modules/apiKeys.ts
@@ -1747,6 +1864,10 @@ var IQAuthClient = class _IQAuthClient {
1747
1864
  this._refreshToken = tokens.refreshToken;
1748
1865
  },
1749
1866
  autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
1867
+ // `'app-state'` is mobile-only — on any other environment we treat it
1868
+ // as the default `true` (proactive refresh ON). Only the mobile client
1869
+ // disables proactive refresh and replaces it with an AppState listener.
1870
+ proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
1750
1871
  onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
1751
1872
  sessionHeaderName: config.sessionHeaderName,
1752
1873
  sessionHeaderValue: config.sessionHeaderValue,
@@ -1787,6 +1908,13 @@ var IQAuthClient = class _IQAuthClient {
1787
1908
  static forServer(config) {
1788
1909
  return new _IQAuthClient({ ...config, environment: "server" });
1789
1910
  }
1911
+ /**
1912
+ * Construct a mobile-environment client. NOTE: this constructor does NOT
1913
+ * subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
1914
+ * is passed — it only disables the per-request proactive refresh. Use
1915
+ * `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
1916
+ * AppState-driven refresh behavior (recommended for Expo / React Native).
1917
+ */
1790
1918
  static forMobile(config) {
1791
1919
  return new _IQAuthClient({ ...config, environment: "mobile" });
1792
1920
  }
@@ -1803,6 +1931,18 @@ var IQAuthClient = class _IQAuthClient {
1803
1931
  getRefreshToken() {
1804
1932
  return this._refreshToken;
1805
1933
  }
1934
+ /**
1935
+ * Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
1936
+ * refresh round-trip on the request hot path doesn't pay the discovery
1937
+ * fetch latency. Safe to call repeatedly. Errors are swallowed; callers
1938
+ * may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
1939
+ */
1940
+ async prewarm() {
1941
+ await Promise.all([
1942
+ this.tokens.prewarm(),
1943
+ this.oidc.getDiscovery().catch(() => void 0)
1944
+ ]);
1945
+ }
1806
1946
  getCurrentClaims() {
1807
1947
  if (!this._accessToken) return null;
1808
1948
  return this.tokens.decode(this._accessToken);
@@ -1815,9 +1955,104 @@ var IQAuthClient = class _IQAuthClient {
1815
1955
  };
1816
1956
 
1817
1957
  // src/mobile.ts
1958
+ function resolveAppState() {
1959
+ try {
1960
+ const rn = Function("return require")()("react-native");
1961
+ return rn?.AppState ?? null;
1962
+ } catch {
1963
+ return null;
1964
+ }
1965
+ }
1818
1966
  var MobileIQAuthClient = class extends IQAuthClient {
1819
1967
  constructor(config) {
1820
1968
  super({ ...config, environment: "mobile" });
1969
+ this.appStateSub = null;
1970
+ this.lastAppState = "active";
1971
+ this.refreshing = false;
1972
+ this.appStateMode = config.autoRefresh === "app-state";
1973
+ this.leewaySeconds = config.appStateRefreshLeewaySeconds ?? 300;
1974
+ this.onTokenRefreshCb = config.onTokenRefresh;
1975
+ this.onAppStateRefreshError = config.onAppStateRefreshError;
1976
+ if (this.appStateMode) {
1977
+ const appState = config.appState !== void 0 ? config.appState : resolveAppState();
1978
+ if (appState) this.startAppStateListener(appState);
1979
+ }
1980
+ }
1981
+ /** True iff the client is configured for AppState-driven refresh. */
1982
+ get isAppStateMode() {
1983
+ return this.appStateMode;
1984
+ }
1985
+ startAppStateListener(appState) {
1986
+ this.lastAppState = appState.currentState ?? "active";
1987
+ const handler = (next) => {
1988
+ const prev = this.lastAppState;
1989
+ this.lastAppState = next;
1990
+ if (next === "active" && prev !== "active") {
1991
+ void this.maybeRefreshOnForeground();
1992
+ }
1993
+ };
1994
+ const sub = appState.addEventListener("change", handler);
1995
+ if (sub && typeof sub.remove === "function") {
1996
+ this.appStateSub = sub;
1997
+ } else if (typeof sub === "function") {
1998
+ this.appStateSub = { remove: sub };
1999
+ } else if (typeof appState.removeEventListener === "function") {
2000
+ this.appStateSub = {
2001
+ remove: () => appState.removeEventListener("change", handler)
2002
+ };
2003
+ } else {
2004
+ this.appStateSub = { remove: () => {
2005
+ } };
2006
+ }
2007
+ }
2008
+ /**
2009
+ * Public hook: call this from your own AppState handler if you've passed
2010
+ * `appState: null` to opt out of the auto-subscription. Returns true if a
2011
+ * refresh was attempted.
2012
+ */
2013
+ async refreshIfStale() {
2014
+ return this.maybeRefreshOnForeground();
2015
+ }
2016
+ async maybeRefreshOnForeground() {
2017
+ if (this.refreshing) return false;
2018
+ if (!this.getRefreshToken()) return false;
2019
+ if (!this.isAccessTokenStale()) return false;
2020
+ const refreshToken = this.getRefreshToken();
2021
+ if (!refreshToken) return false;
2022
+ this.refreshing = true;
2023
+ try {
2024
+ const next = await this.auth.refreshTokens(refreshToken);
2025
+ this.setTokens(next);
2026
+ this.onTokenRefreshCb?.(next);
2027
+ return true;
2028
+ } catch (err) {
2029
+ this.onAppStateRefreshError?.(err);
2030
+ return false;
2031
+ } finally {
2032
+ this.refreshing = false;
2033
+ }
2034
+ }
2035
+ isAccessTokenStale() {
2036
+ const token = this.getAccessToken();
2037
+ if (!token) return true;
2038
+ try {
2039
+ const parts = token.split(".");
2040
+ if (parts.length !== 3) return true;
2041
+ const payload = JSON.parse(
2042
+ typeof atob === "function" ? atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")) : Buffer.from(parts[1], "base64url").toString("utf8")
2043
+ );
2044
+ if (!payload.exp) return false;
2045
+ const now = Math.floor(Date.now() / 1e3);
2046
+ return payload.exp - now < this.leewaySeconds;
2047
+ } catch {
2048
+ return true;
2049
+ }
2050
+ }
2051
+ /** Remove the AppState subscription. Idempotent. */
2052
+ stop() {
2053
+ const sub = this.appStateSub;
2054
+ this.appStateSub = null;
2055
+ sub?.remove();
1821
2056
  }
1822
2057
  };
1823
2058
  function createMobileClient(config) {
package/dist/mobile.mjs CHANGED
@@ -1,17 +1,112 @@
1
1
  import {
2
2
  IQAuthClient
3
- } from "./chunk-W3F4JYGP.mjs";
4
- import "./chunk-UNYDG2L4.mjs";
3
+ } from "./chunk-JXQI62A7.mjs";
4
+ import "./chunk-NUO2I65G.mjs";
5
5
  import {
6
6
  ErrorCodes,
7
7
  IQAuthError
8
- } from "./chunk-6I6RM4MN.mjs";
8
+ } from "./chunk-6PJRLRB4.mjs";
9
9
  import "./chunk-Y6FXYEAI.mjs";
10
10
 
11
11
  // src/mobile.ts
12
+ function resolveAppState() {
13
+ try {
14
+ const rn = Function("return require")()("react-native");
15
+ return rn?.AppState ?? null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
12
20
  var MobileIQAuthClient = class extends IQAuthClient {
13
21
  constructor(config) {
14
22
  super({ ...config, environment: "mobile" });
23
+ this.appStateSub = null;
24
+ this.lastAppState = "active";
25
+ this.refreshing = false;
26
+ this.appStateMode = config.autoRefresh === "app-state";
27
+ this.leewaySeconds = config.appStateRefreshLeewaySeconds ?? 300;
28
+ this.onTokenRefreshCb = config.onTokenRefresh;
29
+ this.onAppStateRefreshError = config.onAppStateRefreshError;
30
+ if (this.appStateMode) {
31
+ const appState = config.appState !== void 0 ? config.appState : resolveAppState();
32
+ if (appState) this.startAppStateListener(appState);
33
+ }
34
+ }
35
+ /** True iff the client is configured for AppState-driven refresh. */
36
+ get isAppStateMode() {
37
+ return this.appStateMode;
38
+ }
39
+ startAppStateListener(appState) {
40
+ this.lastAppState = appState.currentState ?? "active";
41
+ const handler = (next) => {
42
+ const prev = this.lastAppState;
43
+ this.lastAppState = next;
44
+ if (next === "active" && prev !== "active") {
45
+ void this.maybeRefreshOnForeground();
46
+ }
47
+ };
48
+ const sub = appState.addEventListener("change", handler);
49
+ if (sub && typeof sub.remove === "function") {
50
+ this.appStateSub = sub;
51
+ } else if (typeof sub === "function") {
52
+ this.appStateSub = { remove: sub };
53
+ } else if (typeof appState.removeEventListener === "function") {
54
+ this.appStateSub = {
55
+ remove: () => appState.removeEventListener("change", handler)
56
+ };
57
+ } else {
58
+ this.appStateSub = { remove: () => {
59
+ } };
60
+ }
61
+ }
62
+ /**
63
+ * Public hook: call this from your own AppState handler if you've passed
64
+ * `appState: null` to opt out of the auto-subscription. Returns true if a
65
+ * refresh was attempted.
66
+ */
67
+ async refreshIfStale() {
68
+ return this.maybeRefreshOnForeground();
69
+ }
70
+ async maybeRefreshOnForeground() {
71
+ if (this.refreshing) return false;
72
+ if (!this.getRefreshToken()) return false;
73
+ if (!this.isAccessTokenStale()) return false;
74
+ const refreshToken = this.getRefreshToken();
75
+ if (!refreshToken) return false;
76
+ this.refreshing = true;
77
+ try {
78
+ const next = await this.auth.refreshTokens(refreshToken);
79
+ this.setTokens(next);
80
+ this.onTokenRefreshCb?.(next);
81
+ return true;
82
+ } catch (err) {
83
+ this.onAppStateRefreshError?.(err);
84
+ return false;
85
+ } finally {
86
+ this.refreshing = false;
87
+ }
88
+ }
89
+ isAccessTokenStale() {
90
+ const token = this.getAccessToken();
91
+ if (!token) return true;
92
+ try {
93
+ const parts = token.split(".");
94
+ if (parts.length !== 3) return true;
95
+ const payload = JSON.parse(
96
+ typeof atob === "function" ? atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")) : Buffer.from(parts[1], "base64url").toString("utf8")
97
+ );
98
+ if (!payload.exp) return false;
99
+ const now = Math.floor(Date.now() / 1e3);
100
+ return payload.exp - now < this.leewaySeconds;
101
+ } catch {
102
+ return true;
103
+ }
104
+ }
105
+ /** Remove the AppState subscription. Idempotent. */
106
+ stop() {
107
+ const sub = this.appStateSub;
108
+ this.appStateSub = null;
109
+ sub?.remove();
15
110
  }
16
111
  };
17
112
  function createMobileClient(config) {
package/dist/next.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { IQAuthHelperConfig } from './server/handlers.mjs';
2
- import { J as JwtClaims } from './types-DZAflmmq.mjs';
2
+ import { J as JwtClaims } from './types-XOV9XPVi.mjs';
3
+ import './tokens-CITeoG6P.mjs';
3
4
 
4
5
  /**
5
6
  * @iqauth/sdk/next — Next.js (App Router) adapter.
package/dist/next.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { IQAuthHelperConfig } from './server/handlers.js';
2
- import { J as JwtClaims } from './types-DZAflmmq.js';
2
+ import { J as JwtClaims } from './types-XOV9XPVi.js';
3
+ import './tokens-Bqhmqq_R.js';
3
4
 
4
5
  /**
5
6
  * @iqauth/sdk/next — Next.js (App Router) adapter.