@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/server.js CHANGED
@@ -36,24 +36,45 @@ __export(server_exports, {
36
36
  IQAuthClient: () => IQAuthClient,
37
37
  IQAuthError: () => IQAuthError,
38
38
  ServerIQAuthClient: () => ServerIQAuthClient,
39
+ buildUserinfoResponse: () => buildUserinfoResponse,
40
+ createDrizzleLinkAdapter: () => createDrizzleLinkAdapter,
39
41
  createProvisioningBridge: () => createProvisioningBridge,
40
42
  createServerClient: () => createServerClient,
41
43
  handleCallback: () => handleCallback,
42
44
  handleRefresh: () => handleRefresh,
43
45
  handleSignout: () => handleSignout,
46
+ handleUserinfo: () => handleUserinfo,
44
47
  iqAuthMiddleware: () => iqAuthMiddleware,
48
+ linkLocalUserToIqAuthSub: () => linkLocalUserToIqAuthSub,
45
49
  serializeCookie: () => serializeCookie
46
50
  });
47
51
  module.exports = __toCommonJS(server_exports);
48
52
 
49
53
  // src/errors.ts
50
- var IQAuthError = class extends Error {
51
- constructor(code, message, status, raw) {
54
+ var IQAuthError = class _IQAuthError extends Error {
55
+ constructor(code, message, status, cause) {
52
56
  super(message);
53
57
  this.name = "IQAuthError";
54
58
  this.code = code;
55
59
  this.status = status;
56
- this.raw = raw;
60
+ this.cause = cause;
61
+ this.raw = cause;
62
+ }
63
+ /**
64
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
65
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
66
+ */
67
+ static isIQAuthError(value) {
68
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
69
+ }
70
+ /**
71
+ * Type-narrowed code check. Lets callers write
72
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
73
+ * taxonomy without losing the ability to handle server codes via
74
+ * `err.code === "TOKEN_REVOKED"`.
75
+ */
76
+ is(code) {
77
+ return this.code === code;
57
78
  }
58
79
  };
59
80
  var ErrorCodes = {
@@ -204,7 +225,7 @@ var HttpClient = class {
204
225
  headers: this.buildHeaders(),
205
226
  ...this.isBrowserSession() ? { credentials: "include" } : (() => {
206
227
  const refreshToken = this.config.getRefreshToken();
207
- if (!refreshToken) throw new IQAuthError("TOKEN_INVALID", "No refresh token available");
228
+ if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
208
229
  return { body: JSON.stringify({ refreshToken }) };
209
230
  })()
210
231
  });
@@ -221,7 +242,7 @@ var HttpClient = class {
221
242
  return;
222
243
  }
223
244
  if (!body.data.accessToken || !body.data.refreshToken) {
224
- throw new IQAuthError("TOKEN_INVALID", "Refresh response did not include a token pair");
245
+ throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
225
246
  }
226
247
  const tokens = {
227
248
  accessToken: body.data.accessToken,
@@ -239,7 +260,7 @@ var HttpClient = class {
239
260
  return this.requestWithRetry(method, path, body, options, false);
240
261
  }
241
262
  async requestWithRetry(method, path, body, options, hasRetried) {
242
- if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
263
+ if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
243
264
  await this.attemptRefresh();
244
265
  }
245
266
  const url = `${this.config.baseUrl}${path}`;
@@ -467,6 +488,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
467
488
  "iqvalidate"
468
489
  ];
469
490
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
491
+ function classifyJoseError(err) {
492
+ if (err instanceof import_jose.errors.JWTExpired) {
493
+ return { code: "token_expired", message: "Token has expired" };
494
+ }
495
+ if (err instanceof import_jose.errors.JOSEError) {
496
+ return { code: "token_invalid", message: err.message };
497
+ }
498
+ if (err instanceof Error) {
499
+ return { code: "token_invalid", message: err.message };
500
+ }
501
+ return { code: "token_invalid", message: "Token verification failed" };
502
+ }
470
503
  function decodeProtectedHeader(token) {
471
504
  const parts = token.split(".");
472
505
  if (parts.length < 2) return null;
@@ -503,11 +536,11 @@ var TokensModule = class {
503
536
  async verify(token, options = {}) {
504
537
  const header = decodeProtectedHeader(token);
505
538
  if (!header) {
506
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
539
+ throw new IQAuthError("token_invalid", "Unable to decode token");
507
540
  }
508
541
  const kid = header.kid;
509
542
  if (!kid) {
510
- throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
543
+ throw new IQAuthError("token_invalid", "Token missing kid header");
511
544
  }
512
545
  let cache = await this.ensureCache();
513
546
  if (!cache.byKid.has(kid)) {
@@ -515,7 +548,7 @@ var TokensModule = class {
515
548
  cache = await this.ensureCache();
516
549
  }
517
550
  if (!cache.byKid.has(kid)) {
518
- throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
551
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
519
552
  }
520
553
  const issuer = options.issuer ?? this.defaultIssuer;
521
554
  const audience = options.audience ?? this.defaultAudience;
@@ -531,16 +564,8 @@ var TokensModule = class {
531
564
  const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
532
565
  return payload;
533
566
  } catch (err) {
534
- if (err instanceof import_jose.errors.JWTExpired) {
535
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
536
- }
537
- if (err instanceof import_jose.errors.JOSEError) {
538
- throw new IQAuthError("TOKEN_INVALID", err.message);
539
- }
540
- if (err instanceof Error) {
541
- throw new IQAuthError("TOKEN_INVALID", err.message);
542
- }
543
- throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
567
+ const classified = classifyJoseError(err);
568
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
544
569
  }
545
570
  }
546
571
  /**
@@ -582,7 +607,7 @@ var TokensModule = class {
582
607
  getClaims(token) {
583
608
  const claims = this.decode(token);
584
609
  if (!claims) {
585
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
610
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
586
611
  }
587
612
  return claims;
588
613
  }
@@ -592,7 +617,7 @@ var TokensModule = class {
592
617
  }
593
618
  await this.refreshJwks();
594
619
  if (!this.jwksCache) {
595
- throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
620
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
596
621
  }
597
622
  return this.jwksCache;
598
623
  }
@@ -602,22 +627,38 @@ var TokensModule = class {
602
627
  }
603
628
  this.inFlightRefresh = (async () => {
604
629
  try {
605
- const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
630
+ let res;
631
+ try {
632
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
633
+ } catch (err) {
634
+ throw new IQAuthError(
635
+ "network",
636
+ err instanceof Error ? err.message : "JWKS fetch network error",
637
+ void 0,
638
+ err
639
+ );
640
+ }
606
641
  if (!res.ok) {
607
642
  throw new IQAuthError(
608
- "INTERNAL_ERROR",
609
- `Failed to fetch JWKS: ${res.status}`
643
+ "jwks_fetch_failed",
644
+ `Failed to fetch JWKS: ${res.status}`,
645
+ res.status
610
646
  );
611
647
  }
612
648
  let jwks;
613
649
  try {
614
650
  jwks = await res.json();
615
- } catch {
616
- throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
651
+ } catch (err) {
652
+ throw new IQAuthError(
653
+ "jwks_fetch_failed",
654
+ "Malformed JWKS response: invalid JSON",
655
+ res.status,
656
+ err
657
+ );
617
658
  }
618
659
  if (!jwks || !Array.isArray(jwks.keys)) {
619
660
  throw new IQAuthError(
620
- "INTERNAL_ERROR",
661
+ "jwks_fetch_failed",
621
662
  "Malformed JWKS response: expected { keys: [...] }"
622
663
  );
623
664
  }
@@ -625,7 +666,7 @@ var TokensModule = class {
625
666
  for (const key of jwks.keys) {
626
667
  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")) {
627
668
  throw new IQAuthError(
628
- "INTERNAL_ERROR",
669
+ "jwks_fetch_failed",
629
670
  "Malformed JWKS response: key missing required fields"
630
671
  );
631
672
  }
@@ -643,6 +684,19 @@ var TokensModule = class {
643
684
  clearCache() {
644
685
  this.jwksCache = null;
645
686
  }
687
+ /**
688
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
689
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
690
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
691
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
692
+ */
693
+ async prewarm() {
694
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
695
+ try {
696
+ await this.refreshJwks();
697
+ } catch {
698
+ }
699
+ }
646
700
  };
647
701
 
648
702
  // src/modules/sessions.ts
@@ -966,14 +1020,14 @@ var OidcModule = class {
966
1020
  */
967
1021
  async handleCallback(params) {
968
1022
  if (!params.state) {
969
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing state parameter");
1023
+ throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
970
1024
  }
971
1025
  if (!params.code) {
972
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing code parameter");
1026
+ throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
973
1027
  }
974
1028
  const stored = await this.stateStore.get(params.state);
975
1029
  if (!stored) {
976
- throw new IQAuthError("VALIDATION_ERROR", "Unknown or expired OIDC state");
1030
+ throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
977
1031
  }
978
1032
  let tokens;
979
1033
  try {
@@ -991,7 +1045,7 @@ var OidcModule = class {
991
1045
  if (tokens.id_token) {
992
1046
  if (!this.tokensModule) {
993
1047
  throw new IQAuthError(
994
- "INTERNAL_ERROR",
1048
+ "config_invalid",
995
1049
  "OIDC handleCallback received an id_token but no TokensModule is configured for verification"
996
1050
  );
997
1051
  }
@@ -1002,7 +1056,7 @@ var OidcModule = class {
1002
1056
  const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
1003
1057
  if (!tokenNonce || tokenNonce !== stored.nonce) {
1004
1058
  throw new IQAuthError(
1005
- "TOKEN_INVALID",
1059
+ "token_invalid",
1006
1060
  "OIDC id_token nonce did not match the stored value"
1007
1061
  );
1008
1062
  }
@@ -1203,6 +1257,9 @@ var AppsModule = class {
1203
1257
  * @remarks Wraps GET /api/v1/apps/:appKey
1204
1258
  */
1205
1259
  async get(appKey) {
1260
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1261
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1262
+ }
1206
1263
  return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
1207
1264
  }
1208
1265
  /**
@@ -1222,6 +1279,16 @@ var AppsModule = class {
1222
1279
  401
1223
1280
  );
1224
1281
  }
1282
+ if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
1283
+ throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
1284
+ }
1285
+ if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
1286
+ throw new IQAuthError(
1287
+ "ENVIRONMENT_REQUIRED",
1288
+ "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.",
1289
+ 400
1290
+ );
1291
+ }
1225
1292
  return this.http.request("POST", "/api/v1/apps/sync", manifest);
1226
1293
  }
1227
1294
  /**
@@ -1231,11 +1298,14 @@ var AppsModule = class {
1231
1298
  * @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
1232
1299
  */
1233
1300
  async isRegistered(appKey) {
1301
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1302
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1303
+ }
1234
1304
  try {
1235
1305
  await this.get(appKey);
1236
1306
  return true;
1237
1307
  } catch (err) {
1238
- if (err.code === "NOT_FOUND" || err.status === 404) {
1308
+ if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
1239
1309
  return false;
1240
1310
  }
1241
1311
  throw err;
@@ -1272,6 +1342,20 @@ var RolesModule = class {
1272
1342
  };
1273
1343
 
1274
1344
  // src/modules/permissionGroups.ts
1345
+ function assertAppKey(appKey, callsite) {
1346
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1347
+ throw new IQAuthError(
1348
+ "VALIDATION_ERROR",
1349
+ `appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
1350
+ 400
1351
+ );
1352
+ }
1353
+ }
1354
+ function assertNodeKey(nodeKey, callsite) {
1355
+ if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
1356
+ throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
1357
+ }
1358
+ }
1275
1359
  var PermissionGroupsModule = class {
1276
1360
  constructor(http) {
1277
1361
  this.http = http;
@@ -1292,7 +1376,14 @@ var PermissionGroupsModule = class {
1292
1376
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
1293
1377
  }
1294
1378
  async addPermission(tenantId, groupId, data) {
1295
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, data);
1379
+ assertAppKey(data?.appKey, "permissionGroups.addPermission");
1380
+ assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
1381
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
1382
+ appKey: data.appKey,
1383
+ nodeKey: data.nodeKey,
1384
+ effect: data.effect,
1385
+ weight: data.weight
1386
+ });
1296
1387
  }
1297
1388
  async removePermission(tenantId, groupId, permissionId) {
1298
1389
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
@@ -1316,21 +1407,51 @@ var PermissionGroupsModule = class {
1316
1407
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
1317
1408
  }
1318
1409
  async addUserOverride(tenantId, userId, data) {
1319
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, data);
1410
+ assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
1411
+ assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
1412
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
1413
+ appKey: data.appKey,
1414
+ nodeKey: data.nodeKey,
1415
+ effect: data.effect,
1416
+ weight: data.weight,
1417
+ expiresAt: data.expiresAt
1418
+ });
1320
1419
  }
1321
1420
  async removeUserOverride(tenantId, userId, overrideId) {
1322
1421
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
1323
1422
  }
1423
+ /**
1424
+ * Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
1425
+ * longer accepted at the SDK boundary; pass it as `appKey` instead. The
1426
+ * server still accepts `product=` from raw HTTP callers during the
1427
+ * deprecation window, but the SDK will not silently translate it.
1428
+ */
1324
1429
  async getEffectivePermissions(tenantId, userId, params) {
1325
- const query = new URLSearchParams();
1326
- if (params.product) query.set("product", params.product);
1327
- if (params.appKey) query.set("appKey", params.appKey);
1328
- const qs = query.toString();
1329
- return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
1430
+ assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
1431
+ const qs = new URLSearchParams({ appKey: params.appKey }).toString();
1432
+ return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
1330
1433
  }
1331
1434
  async checkPermission(tenantId, userId, appKey, nodeKey) {
1435
+ assertAppKey(appKey, "permissionGroups.checkPermission");
1436
+ assertNodeKey(nodeKey, "permissionGroups.checkPermission");
1332
1437
  return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
1333
1438
  }
1439
+ /**
1440
+ * Task #130 — every entry in `checks` must include a non-empty `appKey`
1441
+ * AND `nodeKey`. The SDK validates the whole batch before sending so a
1442
+ * single misconfigured entry can't slip through and silently report
1443
+ * `allowed: false` from the server's per-entry validation branch.
1444
+ */
1445
+ async batchCheckPermissions(tenantId, userId, checks) {
1446
+ if (!Array.isArray(checks) || checks.length === 0) {
1447
+ throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
1448
+ }
1449
+ checks.forEach((c, i) => {
1450
+ assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
1451
+ assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
1452
+ });
1453
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
1454
+ }
1334
1455
  };
1335
1456
 
1336
1457
  // src/modules/apiKeys.ts
@@ -1755,6 +1876,10 @@ var IQAuthClient = class _IQAuthClient {
1755
1876
  this._refreshToken = tokens.refreshToken;
1756
1877
  },
1757
1878
  autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
1879
+ // `'app-state'` is mobile-only — on any other environment we treat it
1880
+ // as the default `true` (proactive refresh ON). Only the mobile client
1881
+ // disables proactive refresh and replaces it with an AppState listener.
1882
+ proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
1758
1883
  onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
1759
1884
  sessionHeaderName: config.sessionHeaderName,
1760
1885
  sessionHeaderValue: config.sessionHeaderValue,
@@ -1795,6 +1920,13 @@ var IQAuthClient = class _IQAuthClient {
1795
1920
  static forServer(config) {
1796
1921
  return new _IQAuthClient({ ...config, environment: "server" });
1797
1922
  }
1923
+ /**
1924
+ * Construct a mobile-environment client. NOTE: this constructor does NOT
1925
+ * subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
1926
+ * is passed — it only disables the per-request proactive refresh. Use
1927
+ * `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
1928
+ * AppState-driven refresh behavior (recommended for Expo / React Native).
1929
+ */
1798
1930
  static forMobile(config) {
1799
1931
  return new _IQAuthClient({ ...config, environment: "mobile" });
1800
1932
  }
@@ -1811,6 +1943,18 @@ var IQAuthClient = class _IQAuthClient {
1811
1943
  getRefreshToken() {
1812
1944
  return this._refreshToken;
1813
1945
  }
1946
+ /**
1947
+ * Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
1948
+ * refresh round-trip on the request hot path doesn't pay the discovery
1949
+ * fetch latency. Safe to call repeatedly. Errors are swallowed; callers
1950
+ * may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
1951
+ */
1952
+ async prewarm() {
1953
+ await Promise.all([
1954
+ this.tokens.prewarm(),
1955
+ this.oidc.getDiscovery().catch(() => void 0)
1956
+ ]);
1957
+ }
1814
1958
  getCurrentClaims() {
1815
1959
  if (!this._accessToken) return null;
1816
1960
  return this.tokens.decode(this._accessToken);
@@ -1851,14 +1995,14 @@ function assertPublishableKey(raw, opts) {
1851
1995
  const ctx = opts?.context ? `${opts.context}: ` : "";
1852
1996
  if (typeof raw !== "string" || raw.length === 0) {
1853
1997
  throw new IQAuthError(
1854
- "CONFIG_INVALID",
1998
+ "config_invalid",
1855
1999
  `${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.`
1856
2000
  );
1857
2001
  }
1858
2002
  const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
1859
2003
  if (!shapeMatch) {
1860
2004
  throw new IQAuthError(
1861
- "CONFIG_INVALID",
2005
+ "config_invalid",
1862
2006
  `${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.`
1863
2007
  );
1864
2008
  }
@@ -1867,19 +2011,19 @@ function assertPublishableKey(raw, opts) {
1867
2011
  decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
1868
2012
  } catch {
1869
2013
  throw new IQAuthError(
1870
- "CONFIG_INVALID",
2014
+ "config_invalid",
1871
2015
  `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
1872
2016
  );
1873
2017
  }
1874
2018
  if (!isPublishableKeyPayload(decoded)) {
1875
2019
  throw new IQAuthError(
1876
- "CONFIG_INVALID",
2020
+ "config_invalid",
1877
2021
  `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
1878
2022
  );
1879
2023
  }
1880
2024
  if (!isValidIssuerUrl(decoded.iss)) {
1881
2025
  throw new IQAuthError(
1882
- "CONFIG_INVALID",
2026
+ "config_invalid",
1883
2027
  `${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.`
1884
2028
  );
1885
2029
  }
@@ -1893,12 +2037,18 @@ function isPublishableKeyPayload(value) {
1893
2037
 
1894
2038
  // src/middleware/express.ts
1895
2039
  var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
2040
+ // Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
1896
2041
  "TOKEN_INVALID",
1897
2042
  "TOKEN_EXPIRED",
1898
2043
  "TOKEN_REVOKED",
1899
2044
  "SESSION_EXPIRED",
1900
2045
  "SESSION_INVALID",
1901
- "AUTH_REQUIRED"
2046
+ "AUTH_REQUIRED",
2047
+ // Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
2048
+ // Mapped to 401 here so framework consumers don't have to learn the new
2049
+ // codes to keep their auth-failure handling working.
2050
+ "token_invalid",
2051
+ "token_expired"
1902
2052
  ]);
1903
2053
  var DEFAULT_ACCESS_COOKIE = "iqauth_at";
1904
2054
  var DEFAULT_REFRESH_COOKIE = "iqauth_rt";
@@ -2082,6 +2232,41 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
2082
2232
  }
2083
2233
 
2084
2234
  // src/server/handlers.ts
2235
+ async function buildUserinfoResponse(claims, opts = {}) {
2236
+ const baseUser = {
2237
+ sub: claims.sub,
2238
+ email: claims.email,
2239
+ name: claims.name,
2240
+ tenantId: claims.tenantId,
2241
+ vendorId: claims.vendorId,
2242
+ roles: claims.roles ?? [],
2243
+ entitlements: claims.entitlements ?? []
2244
+ };
2245
+ const enriched = opts.enrich ? await opts.enrich(claims) : null;
2246
+ const user = enriched ? { ...baseUser, ...enriched } : baseUser;
2247
+ return {
2248
+ success: true,
2249
+ data: {
2250
+ user,
2251
+ claims,
2252
+ tenantId: claims.tenantId ?? null
2253
+ }
2254
+ };
2255
+ }
2256
+ function emitTiming(cfg, event) {
2257
+ if (cfg.debug) {
2258
+ try {
2259
+ console.debug("[iqauth_helper]", event);
2260
+ } catch {
2261
+ }
2262
+ }
2263
+ if (cfg.onTimingEvent) {
2264
+ try {
2265
+ cfg.onTimingEvent(event);
2266
+ } catch {
2267
+ }
2268
+ }
2269
+ }
2085
2270
  var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
2086
2271
  "TOKEN_REVOKED",
2087
2272
  "SESSION_REVOKED",
@@ -2121,7 +2306,11 @@ function resolve(config) {
2121
2306
  })),
2122
2307
  appId: parsed.appId,
2123
2308
  tenantId: parsed.tenantId,
2124
- clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
2309
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
2310
+ debug: config.debug,
2311
+ onTimingEvent: config.onTimingEvent,
2312
+ signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
2313
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
2125
2314
  };
2126
2315
  }
2127
2316
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -2138,15 +2327,41 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
2138
2327
  }
2139
2328
  function clearCookies(cfg) {
2140
2329
  return [
2141
- makeCookie(cfg, cfg.accessCookieName, "", 0),
2142
- makeCookie(cfg, cfg.refreshCookieName, "", 0)
2330
+ { ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
2331
+ { ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
2143
2332
  ];
2144
2333
  }
2334
+ var DEFAULT_SIGNOUT_TTL_MS = 6e4;
2335
+ var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
2336
+ function pruneInMemoryMarkers(now) {
2337
+ if (inMemorySignoutMarkers.size === 0) return;
2338
+ for (const [k, exp] of inMemorySignoutMarkers) {
2339
+ if (exp <= now) inMemorySignoutMarkers.delete(k);
2340
+ }
2341
+ }
2342
+ var defaultSignoutRegistry = {
2343
+ mark(token, ttlMs) {
2344
+ const now = Date.now();
2345
+ pruneInMemoryMarkers(now);
2346
+ inMemorySignoutMarkers.set(token, now + ttlMs);
2347
+ },
2348
+ has(token) {
2349
+ const now = Date.now();
2350
+ const exp = inMemorySignoutMarkers.get(token);
2351
+ if (!exp) return false;
2352
+ if (exp <= now) {
2353
+ inMemorySignoutMarkers.delete(token);
2354
+ return false;
2355
+ }
2356
+ return true;
2357
+ }
2358
+ };
2145
2359
  function serializeCookie(d) {
2146
2360
  const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
2147
2361
  parts.push(`Path=${d.path}`);
2148
2362
  if (d.domain) parts.push(`Domain=${d.domain}`);
2149
2363
  parts.push(`Max-Age=${d.maxAge}`);
2364
+ if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
2150
2365
  if (d.secure) parts.push("Secure");
2151
2366
  if (d.httpOnly) parts.push("HttpOnly");
2152
2367
  parts.push(`SameSite=${d.sameSite}`);
@@ -2154,7 +2369,9 @@ function serializeCookie(d) {
2154
2369
  }
2155
2370
  async function handleCallback(config, input) {
2156
2371
  const cfg = resolve(config);
2372
+ const t0 = Date.now();
2157
2373
  if (!input.code || !input.redirectUri) {
2374
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
2158
2375
  return {
2159
2376
  status: 400,
2160
2377
  body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
@@ -2162,6 +2379,7 @@ async function handleCallback(config, input) {
2162
2379
  };
2163
2380
  }
2164
2381
  if (!cfg.secretKey) {
2382
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
2165
2383
  return {
2166
2384
  status: 500,
2167
2385
  body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
@@ -2185,6 +2403,7 @@ async function handleCallback(config, input) {
2185
2403
  });
2186
2404
  const json = await res.json().catch(() => ({}));
2187
2405
  if (!res.ok || !json.access_token) {
2406
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
2188
2407
  return {
2189
2408
  status: res.status || 502,
2190
2409
  body: {
@@ -2204,6 +2423,7 @@ async function handleCallback(config, input) {
2204
2423
  if (json.refresh_token) {
2205
2424
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
2206
2425
  }
2426
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
2207
2427
  return {
2208
2428
  status: 200,
2209
2429
  body: { success: true, data: { authenticated: true } },
@@ -2212,8 +2432,18 @@ async function handleCallback(config, input) {
2212
2432
  }
2213
2433
  async function handleRefresh(config, input) {
2214
2434
  const cfg = resolve(config);
2435
+ const t0 = Date.now();
2215
2436
  const refreshToken = input.refreshToken;
2437
+ const idemKey = input.idempotencyToken;
2438
+ if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
2439
+ return {
2440
+ status: 401,
2441
+ body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
2442
+ cookies: clearCookies(cfg)
2443
+ };
2444
+ }
2216
2445
  if (!refreshToken) {
2446
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
2217
2447
  return {
2218
2448
  status: 401,
2219
2449
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
@@ -2229,6 +2459,7 @@ async function handleRefresh(config, input) {
2229
2459
  if (!res.ok || !json.success || !json.data?.accessToken) {
2230
2460
  const status = res.status || 401;
2231
2461
  const errorCode = json.error?.code || "TOKEN_INVALID";
2462
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
2232
2463
  const shouldClear = shouldClearCookiesOnFailure(
2233
2464
  cfg.clearCookiesOnRefreshFailure,
2234
2465
  status,
@@ -2252,6 +2483,7 @@ async function handleRefresh(config, input) {
2252
2483
  if (json.data.refreshToken) {
2253
2484
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
2254
2485
  }
2486
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
2255
2487
  return {
2256
2488
  status: 200,
2257
2489
  body: { success: true, data: { accessToken: json.data.accessToken } },
@@ -2260,6 +2492,10 @@ async function handleRefresh(config, input) {
2260
2492
  }
2261
2493
  async function handleSignout(config, input) {
2262
2494
  const cfg = resolve(config);
2495
+ const t0 = Date.now();
2496
+ if (input.idempotencyToken) {
2497
+ await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
2498
+ }
2263
2499
  if (input.accessToken) {
2264
2500
  try {
2265
2501
  await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
@@ -2281,12 +2517,52 @@ async function handleSignout(config, input) {
2281
2517
  } catch {
2282
2518
  }
2283
2519
  }
2520
+ emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
2284
2521
  return {
2285
2522
  status: 200,
2286
2523
  body: { success: true, data: { signedOut: true } },
2287
2524
  cookies: clearCookies(cfg)
2288
2525
  };
2289
2526
  }
2527
+ var TOKENS_CACHE = /* @__PURE__ */ new Map();
2528
+ function getTokensFor(issuer) {
2529
+ let m = TOKENS_CACHE.get(issuer);
2530
+ if (!m) {
2531
+ m = new TokensModule(issuer);
2532
+ TOKENS_CACHE.set(issuer, m);
2533
+ }
2534
+ return m;
2535
+ }
2536
+ async function handleUserinfo(config, input) {
2537
+ const cfg = resolve(config);
2538
+ if (!input.accessToken) {
2539
+ return {
2540
+ status: 401,
2541
+ body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
2542
+ cookies: []
2543
+ };
2544
+ }
2545
+ let claims;
2546
+ try {
2547
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
2548
+ } catch (err) {
2549
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2550
+ const message = err instanceof Error ? err.message : "Access token verification failed";
2551
+ return {
2552
+ status: 401,
2553
+ body: { success: false, error: { code, message } },
2554
+ cookies: []
2555
+ };
2556
+ }
2557
+ const envelope = await buildUserinfoResponse(claims, {
2558
+ enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
2559
+ });
2560
+ return {
2561
+ status: 200,
2562
+ body: envelope,
2563
+ cookies: []
2564
+ };
2565
+ }
2290
2566
 
2291
2567
  // src/server/provisioningBridge.ts
2292
2568
  function defaultIsUniqueViolation(err) {
@@ -2339,6 +2615,82 @@ function createProvisioningBridge(options) {
2339
2615
  return { ensureUser };
2340
2616
  }
2341
2617
 
2618
+ // src/server/linkLocalUser.ts
2619
+ async function linkLocalUserToIqAuthSub(options) {
2620
+ const { adapter, claims } = options;
2621
+ const lookupBy = options.lookupBy ?? ["email"];
2622
+ const caseInsensitive = options.caseInsensitiveEmail !== false;
2623
+ if (!claims?.sub) throw new Error("linkLocalUserToIqAuthSub: claims.sub is required");
2624
+ return adapter.withTransaction(async (tx) => {
2625
+ const bySub = await tx.findByIqAuthSub(claims.sub);
2626
+ if (bySub) return { status: "already_linked", userId: bySub.id };
2627
+ for (const key of lookupBy) {
2628
+ if (key !== "email") continue;
2629
+ if (!claims.email) continue;
2630
+ const matches = await tx.findByEmail(claims.email, { caseInsensitive });
2631
+ if (matches.length === 0) continue;
2632
+ if (matches.length > 1) {
2633
+ return { status: "conflict", reason: "duplicate_email" };
2634
+ }
2635
+ const row = matches[0];
2636
+ if (row.iqauthSub && row.iqauthSub !== claims.sub) {
2637
+ return { status: "conflict", userId: row.id, reason: "different_sub" };
2638
+ }
2639
+ if (row.iqauthSub === claims.sub) {
2640
+ return { status: "already_linked", userId: row.id };
2641
+ }
2642
+ const wrote = await tx.setIqAuthSub(row.id, claims.sub);
2643
+ if (wrote === false) {
2644
+ return { status: "conflict", userId: row.id, reason: "different_sub" };
2645
+ }
2646
+ return { status: "linked", userId: row.id };
2647
+ }
2648
+ return { status: "not_found" };
2649
+ });
2650
+ }
2651
+ function createDrizzleLinkAdapter(deps) {
2652
+ const { db, table, columns, eq, sql } = deps;
2653
+ const iqauthSubKey = deps.columnNames?.iqauthSub ?? "iqauthSub";
2654
+ return {
2655
+ async withTransaction(fn) {
2656
+ return db.transaction(async (txDb) => {
2657
+ const lockedRead = async (cond, limit) => {
2658
+ const built = txDb.select().from(table).where(cond).limit(limit);
2659
+ if (typeof built.for === "function") {
2660
+ return built.for("update");
2661
+ }
2662
+ return built;
2663
+ };
2664
+ const tx = {
2665
+ async findByIqAuthSub(sub) {
2666
+ const rows = await lockedRead(eq(columns.iqauthSub, sub), 1);
2667
+ return rows[0] ?? null;
2668
+ },
2669
+ async findByEmail(email, { caseInsensitive }) {
2670
+ const cond = caseInsensitive ? sql`lower(${columns.email}) = lower(${email})` : eq(columns.email, email);
2671
+ return lockedRead(cond, 2);
2672
+ },
2673
+ async setIqAuthSub(userId, sub) {
2674
+ const result = await txDb.update(table).set({ [iqauthSubKey]: sub }).where(
2675
+ sql`${columns.id} = ${userId} AND (${columns.iqauthSub} IS NULL OR ${columns.iqauthSub} = ${sub})`
2676
+ );
2677
+ const r = result;
2678
+ if (Array.isArray(r)) {
2679
+ return (r[0]?.affectedRows ?? 1) > 0;
2680
+ }
2681
+ if (r && typeof r === "object") {
2682
+ const n = r.rowCount ?? r.rowsAffected ?? r.changes;
2683
+ if (typeof n === "number") return n > 0;
2684
+ }
2685
+ return true;
2686
+ }
2687
+ };
2688
+ return fn(tx);
2689
+ });
2690
+ }
2691
+ };
2692
+ }
2693
+
2342
2694
  // src/server.ts
2343
2695
  var ServerIQAuthClient = class extends IQAuthClient {
2344
2696
  constructor(config) {
@@ -2359,11 +2711,15 @@ function createServerClient(config) {
2359
2711
  IQAuthClient,
2360
2712
  IQAuthError,
2361
2713
  ServerIQAuthClient,
2714
+ buildUserinfoResponse,
2715
+ createDrizzleLinkAdapter,
2362
2716
  createProvisioningBridge,
2363
2717
  createServerClient,
2364
2718
  handleCallback,
2365
2719
  handleRefresh,
2366
2720
  handleSignout,
2721
+ handleUserinfo,
2367
2722
  iqAuthMiddleware,
2723
+ linkLocalUserToIqAuthSub,
2368
2724
  serializeCookie
2369
2725
  });