@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/index.js CHANGED
@@ -42,10 +42,13 @@ __export(index_exports, {
42
42
  ErrorCodes: () => ErrorCodes,
43
43
  GdprModule: () => GdprModule,
44
44
  HierarchyModule: () => HierarchyModule,
45
+ IQAUTH_SIGNATURE_HEADER: () => IQAUTH_SIGNATURE_HEADER,
45
46
  IQAuthClient: () => IQAuthClient,
46
47
  IQAuthError: () => IQAuthError,
48
+ IQ_AUTH_ERROR_CODES: () => IQ_AUTH_ERROR_CODES,
47
49
  InMemoryOidcStateStore: () => InMemoryOidcStateStore,
48
50
  InvitesModule: () => InvitesModule,
51
+ LEGACY_SIGNATURE_HEADERS: () => LEGACY_SIGNATURE_HEADERS,
49
52
  MembershipsModule: () => MembershipsModule,
50
53
  MfaModule: () => MfaModule,
51
54
  OidcModule: () => OidcModule,
@@ -63,27 +66,61 @@ __export(index_exports, {
63
66
  WebhookSignatureError: () => WebhookSignatureError,
64
67
  WebhooksModule: () => WebhooksModule,
65
68
  assertPublishableKey: () => assertPublishableKey,
69
+ buildUserinfoResponse: () => buildUserinfoResponse,
66
70
  createProvisioningBridge: () => createProvisioningBridge,
67
71
  createTestIssuer: () => createTestIssuer,
68
72
  encodePublishableKey: () => encodePublishableKey,
73
+ expandPermissions: () => expandPermissions,
74
+ handleUserinfo: () => handleUserinfo,
75
+ hasPermission: () => hasPermission,
69
76
  iqAuthMiddleware: () => iqAuthMiddleware,
70
77
  isPublishableKey: () => isPublishableKey,
71
78
  isSecretKey: () => isSecretKey,
72
79
  isValidWebhookSignature: () => isValidWebhookSignature,
73
80
  parsePublishableKey: () => parsePublishableKey,
81
+ parseWebhookEvent: () => parseWebhookEvent,
74
82
  verifyWebhookSignature: () => verifyWebhookSignature,
75
83
  verifyWsUpgrade: () => verifyWsUpgrade
76
84
  });
77
85
  module.exports = __toCommonJS(index_exports);
78
86
 
79
87
  // src/errors.ts
80
- var IQAuthError = class extends Error {
81
- constructor(code, message, status, raw) {
88
+ var IQ_AUTH_ERROR_CODES = [
89
+ "token_expired",
90
+ "token_invalid",
91
+ "jwks_unavailable",
92
+ "jwks_fetch_failed",
93
+ "rate_limited",
94
+ "network",
95
+ "config_invalid",
96
+ "app_not_found",
97
+ "permission_denied",
98
+ "unknown"
99
+ ];
100
+ var IQAuthError = class _IQAuthError extends Error {
101
+ constructor(code, message, status, cause) {
82
102
  super(message);
83
103
  this.name = "IQAuthError";
84
104
  this.code = code;
85
105
  this.status = status;
86
- this.raw = raw;
106
+ this.cause = cause;
107
+ this.raw = cause;
108
+ }
109
+ /**
110
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
111
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
112
+ */
113
+ static isIQAuthError(value) {
114
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
115
+ }
116
+ /**
117
+ * Type-narrowed code check. Lets callers write
118
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
119
+ * taxonomy without losing the ability to handle server codes via
120
+ * `err.code === "TOKEN_REVOKED"`.
121
+ */
122
+ is(code) {
123
+ return this.code === code;
87
124
  }
88
125
  };
89
126
  var ErrorCodes = {
@@ -138,7 +175,7 @@ function resolveRetry(cfg) {
138
175
  }
139
176
  function sleep(ms) {
140
177
  if (ms <= 0) return Promise.resolve();
141
- return new Promise((resolve) => setTimeout(resolve, ms));
178
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
142
179
  }
143
180
  var HttpClient = class {
144
181
  constructor(config) {
@@ -234,7 +271,7 @@ var HttpClient = class {
234
271
  headers: this.buildHeaders(),
235
272
  ...this.isBrowserSession() ? { credentials: "include" } : (() => {
236
273
  const refreshToken = this.config.getRefreshToken();
237
- if (!refreshToken) throw new IQAuthError("TOKEN_INVALID", "No refresh token available");
274
+ if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
238
275
  return { body: JSON.stringify({ refreshToken }) };
239
276
  })()
240
277
  });
@@ -251,7 +288,7 @@ var HttpClient = class {
251
288
  return;
252
289
  }
253
290
  if (!body.data.accessToken || !body.data.refreshToken) {
254
- throw new IQAuthError("TOKEN_INVALID", "Refresh response did not include a token pair");
291
+ throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
255
292
  }
256
293
  const tokens = {
257
294
  accessToken: body.data.accessToken,
@@ -269,7 +306,7 @@ var HttpClient = class {
269
306
  return this.requestWithRetry(method, path, body, options, false);
270
307
  }
271
308
  async requestWithRetry(method, path, body, options, hasRetried) {
272
- if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
309
+ if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
273
310
  await this.attemptRefresh();
274
311
  }
275
312
  const url = `${this.config.baseUrl}${path}`;
@@ -497,6 +534,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
497
534
  "iqvalidate"
498
535
  ];
499
536
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
537
+ function classifyJoseError(err) {
538
+ if (err instanceof import_jose.errors.JWTExpired) {
539
+ return { code: "token_expired", message: "Token has expired" };
540
+ }
541
+ if (err instanceof import_jose.errors.JOSEError) {
542
+ return { code: "token_invalid", message: err.message };
543
+ }
544
+ if (err instanceof Error) {
545
+ return { code: "token_invalid", message: err.message };
546
+ }
547
+ return { code: "token_invalid", message: "Token verification failed" };
548
+ }
500
549
  function decodeProtectedHeader(token) {
501
550
  const parts = token.split(".");
502
551
  if (parts.length < 2) return null;
@@ -533,11 +582,11 @@ var TokensModule = class {
533
582
  async verify(token, options = {}) {
534
583
  const header = decodeProtectedHeader(token);
535
584
  if (!header) {
536
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
585
+ throw new IQAuthError("token_invalid", "Unable to decode token");
537
586
  }
538
587
  const kid = header.kid;
539
588
  if (!kid) {
540
- throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
589
+ throw new IQAuthError("token_invalid", "Token missing kid header");
541
590
  }
542
591
  let cache = await this.ensureCache();
543
592
  if (!cache.byKid.has(kid)) {
@@ -545,7 +594,7 @@ var TokensModule = class {
545
594
  cache = await this.ensureCache();
546
595
  }
547
596
  if (!cache.byKid.has(kid)) {
548
- throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
597
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
549
598
  }
550
599
  const issuer = options.issuer ?? this.defaultIssuer;
551
600
  const audience = options.audience ?? this.defaultAudience;
@@ -561,16 +610,8 @@ var TokensModule = class {
561
610
  const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
562
611
  return payload;
563
612
  } catch (err) {
564
- if (err instanceof import_jose.errors.JWTExpired) {
565
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
566
- }
567
- if (err instanceof import_jose.errors.JOSEError) {
568
- throw new IQAuthError("TOKEN_INVALID", err.message);
569
- }
570
- if (err instanceof Error) {
571
- throw new IQAuthError("TOKEN_INVALID", err.message);
572
- }
573
- throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
613
+ const classified = classifyJoseError(err);
614
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
574
615
  }
575
616
  }
576
617
  /**
@@ -612,7 +653,7 @@ var TokensModule = class {
612
653
  getClaims(token) {
613
654
  const claims = this.decode(token);
614
655
  if (!claims) {
615
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
656
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
616
657
  }
617
658
  return claims;
618
659
  }
@@ -622,7 +663,7 @@ var TokensModule = class {
622
663
  }
623
664
  await this.refreshJwks();
624
665
  if (!this.jwksCache) {
625
- throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
666
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
626
667
  }
627
668
  return this.jwksCache;
628
669
  }
@@ -632,22 +673,38 @@ var TokensModule = class {
632
673
  }
633
674
  this.inFlightRefresh = (async () => {
634
675
  try {
635
- const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
676
+ let res;
677
+ try {
678
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
679
+ } catch (err) {
680
+ throw new IQAuthError(
681
+ "network",
682
+ err instanceof Error ? err.message : "JWKS fetch network error",
683
+ void 0,
684
+ err
685
+ );
686
+ }
636
687
  if (!res.ok) {
637
688
  throw new IQAuthError(
638
- "INTERNAL_ERROR",
639
- `Failed to fetch JWKS: ${res.status}`
689
+ "jwks_fetch_failed",
690
+ `Failed to fetch JWKS: ${res.status}`,
691
+ res.status
640
692
  );
641
693
  }
642
694
  let jwks;
643
695
  try {
644
696
  jwks = await res.json();
645
- } catch {
646
- throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
697
+ } catch (err) {
698
+ throw new IQAuthError(
699
+ "jwks_fetch_failed",
700
+ "Malformed JWKS response: invalid JSON",
701
+ res.status,
702
+ err
703
+ );
647
704
  }
648
705
  if (!jwks || !Array.isArray(jwks.keys)) {
649
706
  throw new IQAuthError(
650
- "INTERNAL_ERROR",
707
+ "jwks_fetch_failed",
651
708
  "Malformed JWKS response: expected { keys: [...] }"
652
709
  );
653
710
  }
@@ -655,7 +712,7 @@ var TokensModule = class {
655
712
  for (const key of jwks.keys) {
656
713
  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")) {
657
714
  throw new IQAuthError(
658
- "INTERNAL_ERROR",
715
+ "jwks_fetch_failed",
659
716
  "Malformed JWKS response: key missing required fields"
660
717
  );
661
718
  }
@@ -673,6 +730,19 @@ var TokensModule = class {
673
730
  clearCache() {
674
731
  this.jwksCache = null;
675
732
  }
733
+ /**
734
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
735
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
736
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
737
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
738
+ */
739
+ async prewarm() {
740
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
741
+ try {
742
+ await this.refreshJwks();
743
+ } catch {
744
+ }
745
+ }
676
746
  };
677
747
 
678
748
  // src/modules/sessions.ts
@@ -996,14 +1066,14 @@ var OidcModule = class {
996
1066
  */
997
1067
  async handleCallback(params) {
998
1068
  if (!params.state) {
999
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing state parameter");
1069
+ throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
1000
1070
  }
1001
1071
  if (!params.code) {
1002
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing code parameter");
1072
+ throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
1003
1073
  }
1004
1074
  const stored = await this.stateStore.get(params.state);
1005
1075
  if (!stored) {
1006
- throw new IQAuthError("VALIDATION_ERROR", "Unknown or expired OIDC state");
1076
+ throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
1007
1077
  }
1008
1078
  let tokens;
1009
1079
  try {
@@ -1021,7 +1091,7 @@ var OidcModule = class {
1021
1091
  if (tokens.id_token) {
1022
1092
  if (!this.tokensModule) {
1023
1093
  throw new IQAuthError(
1024
- "INTERNAL_ERROR",
1094
+ "config_invalid",
1025
1095
  "OIDC handleCallback received an id_token but no TokensModule is configured for verification"
1026
1096
  );
1027
1097
  }
@@ -1032,7 +1102,7 @@ var OidcModule = class {
1032
1102
  const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
1033
1103
  if (!tokenNonce || tokenNonce !== stored.nonce) {
1034
1104
  throw new IQAuthError(
1035
- "TOKEN_INVALID",
1105
+ "token_invalid",
1036
1106
  "OIDC id_token nonce did not match the stored value"
1037
1107
  );
1038
1108
  }
@@ -1233,6 +1303,9 @@ var AppsModule = class {
1233
1303
  * @remarks Wraps GET /api/v1/apps/:appKey
1234
1304
  */
1235
1305
  async get(appKey) {
1306
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1307
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1308
+ }
1236
1309
  return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
1237
1310
  }
1238
1311
  /**
@@ -1252,6 +1325,16 @@ var AppsModule = class {
1252
1325
  401
1253
1326
  );
1254
1327
  }
1328
+ if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
1329
+ throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
1330
+ }
1331
+ if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
1332
+ throw new IQAuthError(
1333
+ "ENVIRONMENT_REQUIRED",
1334
+ "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.",
1335
+ 400
1336
+ );
1337
+ }
1255
1338
  return this.http.request("POST", "/api/v1/apps/sync", manifest);
1256
1339
  }
1257
1340
  /**
@@ -1261,11 +1344,14 @@ var AppsModule = class {
1261
1344
  * @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
1262
1345
  */
1263
1346
  async isRegistered(appKey) {
1347
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1348
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1349
+ }
1264
1350
  try {
1265
1351
  await this.get(appKey);
1266
1352
  return true;
1267
1353
  } catch (err) {
1268
- if (err.code === "NOT_FOUND" || err.status === 404) {
1354
+ if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
1269
1355
  return false;
1270
1356
  }
1271
1357
  throw err;
@@ -1302,6 +1388,20 @@ var RolesModule = class {
1302
1388
  };
1303
1389
 
1304
1390
  // src/modules/permissionGroups.ts
1391
+ function assertAppKey(appKey, callsite) {
1392
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1393
+ throw new IQAuthError(
1394
+ "VALIDATION_ERROR",
1395
+ `appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
1396
+ 400
1397
+ );
1398
+ }
1399
+ }
1400
+ function assertNodeKey(nodeKey, callsite) {
1401
+ if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
1402
+ throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
1403
+ }
1404
+ }
1305
1405
  var PermissionGroupsModule = class {
1306
1406
  constructor(http) {
1307
1407
  this.http = http;
@@ -1322,7 +1422,14 @@ var PermissionGroupsModule = class {
1322
1422
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
1323
1423
  }
1324
1424
  async addPermission(tenantId, groupId, data) {
1325
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, data);
1425
+ assertAppKey(data?.appKey, "permissionGroups.addPermission");
1426
+ assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
1427
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
1428
+ appKey: data.appKey,
1429
+ nodeKey: data.nodeKey,
1430
+ effect: data.effect,
1431
+ weight: data.weight
1432
+ });
1326
1433
  }
1327
1434
  async removePermission(tenantId, groupId, permissionId) {
1328
1435
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
@@ -1346,21 +1453,51 @@ var PermissionGroupsModule = class {
1346
1453
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
1347
1454
  }
1348
1455
  async addUserOverride(tenantId, userId, data) {
1349
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, data);
1456
+ assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
1457
+ assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
1458
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
1459
+ appKey: data.appKey,
1460
+ nodeKey: data.nodeKey,
1461
+ effect: data.effect,
1462
+ weight: data.weight,
1463
+ expiresAt: data.expiresAt
1464
+ });
1350
1465
  }
1351
1466
  async removeUserOverride(tenantId, userId, overrideId) {
1352
1467
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
1353
1468
  }
1469
+ /**
1470
+ * Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
1471
+ * longer accepted at the SDK boundary; pass it as `appKey` instead. The
1472
+ * server still accepts `product=` from raw HTTP callers during the
1473
+ * deprecation window, but the SDK will not silently translate it.
1474
+ */
1354
1475
  async getEffectivePermissions(tenantId, userId, params) {
1355
- const query = new URLSearchParams();
1356
- if (params.product) query.set("product", params.product);
1357
- if (params.appKey) query.set("appKey", params.appKey);
1358
- const qs = query.toString();
1359
- return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
1476
+ assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
1477
+ const qs = new URLSearchParams({ appKey: params.appKey }).toString();
1478
+ return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
1360
1479
  }
1361
1480
  async checkPermission(tenantId, userId, appKey, nodeKey) {
1481
+ assertAppKey(appKey, "permissionGroups.checkPermission");
1482
+ assertNodeKey(nodeKey, "permissionGroups.checkPermission");
1362
1483
  return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
1363
1484
  }
1485
+ /**
1486
+ * Task #130 — every entry in `checks` must include a non-empty `appKey`
1487
+ * AND `nodeKey`. The SDK validates the whole batch before sending so a
1488
+ * single misconfigured entry can't slip through and silently report
1489
+ * `allowed: false` from the server's per-entry validation branch.
1490
+ */
1491
+ async batchCheckPermissions(tenantId, userId, checks) {
1492
+ if (!Array.isArray(checks) || checks.length === 0) {
1493
+ throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
1494
+ }
1495
+ checks.forEach((c, i) => {
1496
+ assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
1497
+ assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
1498
+ });
1499
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
1500
+ }
1364
1501
  };
1365
1502
 
1366
1503
  // src/modules/apiKeys.ts
@@ -1785,6 +1922,10 @@ var IQAuthClient = class _IQAuthClient {
1785
1922
  this._refreshToken = tokens.refreshToken;
1786
1923
  },
1787
1924
  autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
1925
+ // `'app-state'` is mobile-only — on any other environment we treat it
1926
+ // as the default `true` (proactive refresh ON). Only the mobile client
1927
+ // disables proactive refresh and replaces it with an AppState listener.
1928
+ proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
1788
1929
  onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
1789
1930
  sessionHeaderName: config.sessionHeaderName,
1790
1931
  sessionHeaderValue: config.sessionHeaderValue,
@@ -1825,6 +1966,13 @@ var IQAuthClient = class _IQAuthClient {
1825
1966
  static forServer(config) {
1826
1967
  return new _IQAuthClient({ ...config, environment: "server" });
1827
1968
  }
1969
+ /**
1970
+ * Construct a mobile-environment client. NOTE: this constructor does NOT
1971
+ * subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
1972
+ * is passed — it only disables the per-request proactive refresh. Use
1973
+ * `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
1974
+ * AppState-driven refresh behavior (recommended for Expo / React Native).
1975
+ */
1828
1976
  static forMobile(config) {
1829
1977
  return new _IQAuthClient({ ...config, environment: "mobile" });
1830
1978
  }
@@ -1841,6 +1989,18 @@ var IQAuthClient = class _IQAuthClient {
1841
1989
  getRefreshToken() {
1842
1990
  return this._refreshToken;
1843
1991
  }
1992
+ /**
1993
+ * Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
1994
+ * refresh round-trip on the request hot path doesn't pay the discovery
1995
+ * fetch latency. Safe to call repeatedly. Errors are swallowed; callers
1996
+ * may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
1997
+ */
1998
+ async prewarm() {
1999
+ await Promise.all([
2000
+ this.tokens.prewarm(),
2001
+ this.oidc.getDiscovery().catch(() => void 0)
2002
+ ]);
2003
+ }
1844
2004
  getCurrentClaims() {
1845
2005
  if (!this._accessToken) return null;
1846
2006
  return this.tokens.decode(this._accessToken);
@@ -1911,14 +2071,14 @@ function assertPublishableKey(raw, opts) {
1911
2071
  const ctx = opts?.context ? `${opts.context}: ` : "";
1912
2072
  if (typeof raw !== "string" || raw.length === 0) {
1913
2073
  throw new IQAuthError(
1914
- "CONFIG_INVALID",
2074
+ "config_invalid",
1915
2075
  `${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.`
1916
2076
  );
1917
2077
  }
1918
2078
  const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
1919
2079
  if (!shapeMatch) {
1920
2080
  throw new IQAuthError(
1921
- "CONFIG_INVALID",
2081
+ "config_invalid",
1922
2082
  `${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.`
1923
2083
  );
1924
2084
  }
@@ -1927,19 +2087,19 @@ function assertPublishableKey(raw, opts) {
1927
2087
  decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
1928
2088
  } catch {
1929
2089
  throw new IQAuthError(
1930
- "CONFIG_INVALID",
2090
+ "config_invalid",
1931
2091
  `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
1932
2092
  );
1933
2093
  }
1934
2094
  if (!isPublishableKeyPayload(decoded)) {
1935
2095
  throw new IQAuthError(
1936
- "CONFIG_INVALID",
2096
+ "config_invalid",
1937
2097
  `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
1938
2098
  );
1939
2099
  }
1940
2100
  if (!isValidIssuerUrl(decoded.iss)) {
1941
2101
  throw new IQAuthError(
1942
- "CONFIG_INVALID",
2102
+ "config_invalid",
1943
2103
  `${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.`
1944
2104
  );
1945
2105
  }
@@ -1959,12 +2119,18 @@ function isSecretKey(raw) {
1959
2119
 
1960
2120
  // src/middleware/express.ts
1961
2121
  var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
2122
+ // Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
1962
2123
  "TOKEN_INVALID",
1963
2124
  "TOKEN_EXPIRED",
1964
2125
  "TOKEN_REVOKED",
1965
2126
  "SESSION_EXPIRED",
1966
2127
  "SESSION_INVALID",
1967
- "AUTH_REQUIRED"
2128
+ "AUTH_REQUIRED",
2129
+ // Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
2130
+ // Mapped to 401 here so framework consumers don't have to learn the new
2131
+ // codes to keep their auth-failure handling working.
2132
+ "token_invalid",
2133
+ "token_expired"
1968
2134
  ]);
1969
2135
  var DEFAULT_ACCESS_COOKIE = "iqauth_at";
1970
2136
  function getAuthorizationHeader(req) {
@@ -2146,6 +2312,185 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
2146
2312
  };
2147
2313
  }
2148
2314
 
2315
+ // src/server/handlers.ts
2316
+ async function buildUserinfoResponse(claims, opts = {}) {
2317
+ const baseUser = {
2318
+ sub: claims.sub,
2319
+ email: claims.email,
2320
+ name: claims.name,
2321
+ tenantId: claims.tenantId,
2322
+ vendorId: claims.vendorId,
2323
+ roles: claims.roles ?? [],
2324
+ entitlements: claims.entitlements ?? []
2325
+ };
2326
+ const enriched = opts.enrich ? await opts.enrich(claims) : null;
2327
+ const user = enriched ? { ...baseUser, ...enriched } : baseUser;
2328
+ return {
2329
+ success: true,
2330
+ data: {
2331
+ user,
2332
+ claims,
2333
+ tenantId: claims.tenantId ?? null
2334
+ }
2335
+ };
2336
+ }
2337
+ var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
2338
+ var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
2339
+ function resolve(config) {
2340
+ const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
2341
+ const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
2342
+ return {
2343
+ publishableKey: config.publishableKey,
2344
+ secretKey: config.secretKey,
2345
+ issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
2346
+ accessCookieName: config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at",
2347
+ refreshCookieName: config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt",
2348
+ cookieDomain: config.cookieDomain,
2349
+ sameSite: config.sameSite ?? "lax",
2350
+ secure: config.secure ?? true,
2351
+ cookiePath: config.cookiePath ?? "/",
2352
+ tokenPath: config.tokenPath ?? "/oidc/token",
2353
+ refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
2354
+ logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
2355
+ fetchImpl: config.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
2356
+ throw new Error("global fetch is unavailable; pass fetchImpl");
2357
+ })),
2358
+ appId: parsed.appId,
2359
+ tenantId: parsed.tenantId,
2360
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
2361
+ debug: config.debug,
2362
+ onTimingEvent: config.onTimingEvent,
2363
+ signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
2364
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
2365
+ };
2366
+ }
2367
+ var DEFAULT_SIGNOUT_TTL_MS = 6e4;
2368
+ var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
2369
+ function pruneInMemoryMarkers(now) {
2370
+ if (inMemorySignoutMarkers.size === 0) return;
2371
+ for (const [k, exp] of inMemorySignoutMarkers) {
2372
+ if (exp <= now) inMemorySignoutMarkers.delete(k);
2373
+ }
2374
+ }
2375
+ var defaultSignoutRegistry = {
2376
+ mark(token, ttlMs) {
2377
+ const now = Date.now();
2378
+ pruneInMemoryMarkers(now);
2379
+ inMemorySignoutMarkers.set(token, now + ttlMs);
2380
+ },
2381
+ has(token) {
2382
+ const now = Date.now();
2383
+ const exp = inMemorySignoutMarkers.get(token);
2384
+ if (!exp) return false;
2385
+ if (exp <= now) {
2386
+ inMemorySignoutMarkers.delete(token);
2387
+ return false;
2388
+ }
2389
+ return true;
2390
+ }
2391
+ };
2392
+ var TOKENS_CACHE = /* @__PURE__ */ new Map();
2393
+ function getTokensFor(issuer) {
2394
+ let m = TOKENS_CACHE.get(issuer);
2395
+ if (!m) {
2396
+ m = new TokensModule(issuer);
2397
+ TOKENS_CACHE.set(issuer, m);
2398
+ }
2399
+ return m;
2400
+ }
2401
+ async function handleUserinfo(config, input) {
2402
+ const cfg = resolve(config);
2403
+ if (!input.accessToken) {
2404
+ return {
2405
+ status: 401,
2406
+ body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
2407
+ cookies: []
2408
+ };
2409
+ }
2410
+ let claims;
2411
+ try {
2412
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
2413
+ } catch (err) {
2414
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2415
+ const message = err instanceof Error ? err.message : "Access token verification failed";
2416
+ return {
2417
+ status: 401,
2418
+ body: { success: false, error: { code, message } },
2419
+ cookies: []
2420
+ };
2421
+ }
2422
+ const envelope = await buildUserinfoResponse(claims, {
2423
+ enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
2424
+ });
2425
+ return {
2426
+ status: 200,
2427
+ body: envelope,
2428
+ cookies: []
2429
+ };
2430
+ }
2431
+
2432
+ // src/permissions/wildcard.ts
2433
+ var SUFFIX = ".*";
2434
+ function wildcardPrefix(pattern) {
2435
+ return pattern.slice(0, -SUFFIX.length);
2436
+ }
2437
+ function hasPermission(set, id) {
2438
+ if (!id) return false;
2439
+ if (!set) return false;
2440
+ if (id === "*") {
2441
+ for (const entry of set) if (entry === "*") return true;
2442
+ return false;
2443
+ }
2444
+ const queryIsWildcard = id.endsWith(SUFFIX);
2445
+ const queryPrefix = queryIsWildcard ? wildcardPrefix(id) : null;
2446
+ for (const entry of set) {
2447
+ if (!entry) continue;
2448
+ if (entry === "*") return true;
2449
+ if (entry === id) return true;
2450
+ if (entry.endsWith(SUFFIX)) {
2451
+ const prefix = wildcardPrefix(entry);
2452
+ if (!queryIsWildcard) {
2453
+ if (id === prefix) return true;
2454
+ if (id.startsWith(prefix + ".")) return true;
2455
+ } else {
2456
+ if (queryPrefix === prefix) return true;
2457
+ if (queryPrefix !== null && queryPrefix.startsWith(prefix + ".")) return true;
2458
+ }
2459
+ }
2460
+ }
2461
+ return false;
2462
+ }
2463
+ function expandPermissions(set) {
2464
+ if (!set) return [];
2465
+ const seen = /* @__PURE__ */ new Set();
2466
+ for (const raw of set) {
2467
+ if (typeof raw !== "string" || raw.length === 0) continue;
2468
+ seen.add(raw);
2469
+ }
2470
+ if (seen.has("*")) return ["*"];
2471
+ const wildcards = [];
2472
+ for (const entry of seen) if (entry.endsWith(SUFFIX)) wildcards.push(entry);
2473
+ const out = [];
2474
+ for (const entry of seen) {
2475
+ let covered = false;
2476
+ for (const w of wildcards) {
2477
+ if (w === entry) continue;
2478
+ const prefix = wildcardPrefix(w);
2479
+ if (entry === prefix) {
2480
+ covered = true;
2481
+ break;
2482
+ }
2483
+ if (entry.startsWith(prefix + ".")) {
2484
+ covered = true;
2485
+ break;
2486
+ }
2487
+ }
2488
+ if (!covered) out.push(entry);
2489
+ }
2490
+ out.sort();
2491
+ return out;
2492
+ }
2493
+
2149
2494
  // src/ws.ts
2150
2495
  var DEFAULT_COOKIE = "iqauth_at";
2151
2496
  var DEFAULT_SUBPROTOCOL_PREFIX = "iqauth.bearer.";
@@ -2244,10 +2589,10 @@ function jwkFromPublicKey(publicKey, kid) {
2244
2589
  return { kty: "RSA", use: "sig", alg: "RS256", kid, n: jwk.n, e: jwk.e };
2245
2590
  }
2246
2591
  function readBody(req) {
2247
- return new Promise((resolve, reject) => {
2592
+ return new Promise((resolve2, reject) => {
2248
2593
  const chunks = [];
2249
2594
  req.on("data", (c) => chunks.push(c));
2250
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
2595
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
2251
2596
  req.on("error", reject);
2252
2597
  });
2253
2598
  }
@@ -2435,11 +2780,11 @@ async function createTestIssuer(options = {}) {
2435
2780
  const server = (0, import_http2.createServer)((req, res) => {
2436
2781
  void handler(req, res);
2437
2782
  });
2438
- await new Promise((resolve, reject) => {
2783
+ await new Promise((resolve2, reject) => {
2439
2784
  server.once("error", reject);
2440
2785
  server.listen(port, host, () => {
2441
2786
  server.off("error", reject);
2442
- resolve();
2787
+ resolve2();
2443
2788
  });
2444
2789
  });
2445
2790
  const addr = server.address();
@@ -2463,8 +2808,8 @@ async function createTestIssuer(options = {}) {
2463
2808
  pendingCodes.set(code, { claims: opts, refreshToken });
2464
2809
  return code;
2465
2810
  },
2466
- close: () => new Promise((resolve, reject) => {
2467
- server.close((err) => err ? reject(err) : resolve());
2811
+ close: () => new Promise((resolve2, reject) => {
2812
+ server.close((err) => err ? reject(err) : resolve2());
2468
2813
  })
2469
2814
  };
2470
2815
  }
@@ -2478,6 +2823,12 @@ var WebhookSignatureError = class extends Error {
2478
2823
  this.code = code;
2479
2824
  }
2480
2825
  };
2826
+ var IQAUTH_SIGNATURE_HEADER = "x-iqauth-signature";
2827
+ var LEGACY_SIGNATURE_HEADERS = [
2828
+ "x-webhook-signature",
2829
+ "x-iq-auth-signature",
2830
+ "x-signature"
2831
+ ];
2481
2832
  function toBuffer(p) {
2482
2833
  if (typeof p === "string") return Buffer.from(p, "utf8");
2483
2834
  if (Buffer.isBuffer(p)) return p;
@@ -2486,13 +2837,19 @@ function toBuffer(p) {
2486
2837
  function parseHeader(header) {
2487
2838
  let t = NaN;
2488
2839
  const v1 = [];
2489
- for (const part of header.split(",")) {
2490
- const [k, v] = part.split("=", 2);
2491
- if (!k || v === void 0) continue;
2492
- const key = k.trim();
2493
- const value = v.trim();
2840
+ const trimmed = header.trim();
2841
+ if (/^[0-9a-f]+$/i.test(trimmed)) {
2842
+ v1.push(trimmed.toLowerCase());
2843
+ return { t, v1 };
2844
+ }
2845
+ for (const part of trimmed.split(",")) {
2846
+ const eqIdx = part.indexOf("=");
2847
+ if (eqIdx === -1) continue;
2848
+ const key = part.slice(0, eqIdx).trim().toLowerCase();
2849
+ const value = part.slice(eqIdx + 1).trim();
2850
+ if (!value) continue;
2494
2851
  if (key === "t") t = Number(value);
2495
- else if (key === "v1") v1.push(value);
2852
+ else if (key === "v1") v1.push(value.toLowerCase());
2496
2853
  }
2497
2854
  return { t, v1 };
2498
2855
  }
@@ -2504,6 +2861,11 @@ function timingSafeEqualHex(a, b) {
2504
2861
  return false;
2505
2862
  }
2506
2863
  }
2864
+ function computeSignatures(secret, body, t) {
2865
+ const modern = import_crypto3.default.createHmac("sha256", secret).update(body).digest("hex");
2866
+ const legacy = Number.isFinite(t) ? import_crypto3.default.createHmac("sha256", secret).update(`${t}.`).update(body).digest("hex") : null;
2867
+ return { modern, legacy };
2868
+ }
2507
2869
  function verifyWebhookSignature(opts) {
2508
2870
  const headerRaw = Array.isArray(opts.header) ? opts.header[0] : opts.header;
2509
2871
  if (!headerRaw || typeof headerRaw !== "string") {
@@ -2513,20 +2875,27 @@ function verifyWebhookSignature(opts) {
2513
2875
  throw new WebhookSignatureError("MISSING_SECRET", "secret is required");
2514
2876
  }
2515
2877
  const { t, v1 } = parseHeader(headerRaw);
2516
- if (!Number.isFinite(t) || v1.length === 0) {
2878
+ if (v1.length === 0) {
2517
2879
  throw new WebhookSignatureError("MALFORMED_HEADER", `Could not parse signature header: ${headerRaw}`);
2518
2880
  }
2519
2881
  const tolerance = opts.toleranceSeconds ?? 300;
2520
- const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
2521
- if (Math.abs(now - t) > tolerance) {
2522
- throw new WebhookSignatureError(
2523
- "TIMESTAMP_OUT_OF_TOLERANCE",
2524
- `Signature timestamp ${t} is outside the ${tolerance}s tolerance window (now=${now})`
2525
- );
2882
+ if (Number.isFinite(t)) {
2883
+ const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
2884
+ if (Math.abs(now - t) > tolerance) {
2885
+ throw new WebhookSignatureError(
2886
+ "TIMESTAMP_OUT_OF_TOLERANCE",
2887
+ `Signature timestamp ${t} is outside the ${tolerance}s tolerance window (now=${now})`
2888
+ );
2889
+ }
2526
2890
  }
2527
2891
  const body = toBuffer(opts.payload);
2528
- const expected = import_crypto3.default.createHmac("sha256", opts.secret).update(`${t}.`).update(body).digest("hex");
2529
- const matched = v1.some((sig) => timingSafeEqualHex(sig, expected));
2892
+ const { modern, legacy } = computeSignatures(opts.secret, body, t);
2893
+ const matched = v1.some((sig) => {
2894
+ const lower = sig.toLowerCase();
2895
+ if (timingSafeEqualHex(lower, modern)) return true;
2896
+ if (legacy && timingSafeEqualHex(lower, legacy)) return true;
2897
+ return false;
2898
+ });
2530
2899
  if (!matched) {
2531
2900
  throw new WebhookSignatureError("SIGNATURE_MISMATCH", "Webhook signature does not match expected value");
2532
2901
  }
@@ -2546,6 +2915,125 @@ function isValidWebhookSignature(opts) {
2546
2915
  return false;
2547
2916
  }
2548
2917
  }
2918
+ function readHeader(headers, name) {
2919
+ if (typeof headers.get === "function") {
2920
+ return headers.get(name);
2921
+ }
2922
+ const lower = name.toLowerCase();
2923
+ const obj = headers;
2924
+ if (lower in obj) return obj[lower];
2925
+ for (const [k, v] of Object.entries(obj)) {
2926
+ if (k.toLowerCase() === lower) return v;
2927
+ }
2928
+ return void 0;
2929
+ }
2930
+ function pickHeaderValue(value) {
2931
+ if (value == null) return null;
2932
+ if (Array.isArray(value)) return value[0] ?? null;
2933
+ return value;
2934
+ }
2935
+ function envelopeError(message) {
2936
+ throw new WebhookSignatureError("MALFORMED_ENVELOPE", message);
2937
+ }
2938
+ function parseWebhookEvent(rawBody, headers, secrets, opts = {}) {
2939
+ if (!Array.isArray(secrets) || secrets.length === 0 || secrets.every((s) => !s)) {
2940
+ throw new WebhookSignatureError("MISSING_SECRET", "At least one signing secret is required");
2941
+ }
2942
+ let headerValue = pickHeaderValue(readHeader(headers, IQAUTH_SIGNATURE_HEADER));
2943
+ let usedHeader = IQAUTH_SIGNATURE_HEADER;
2944
+ if (!headerValue) {
2945
+ for (const legacy of LEGACY_SIGNATURE_HEADERS) {
2946
+ const v = pickHeaderValue(readHeader(headers, legacy));
2947
+ if (v) {
2948
+ headerValue = v;
2949
+ usedHeader = legacy;
2950
+ const log = opts.onDeprecation ?? ((m) => console.warn(m));
2951
+ log(
2952
+ `[iqauth] deprecation: webhook delivery used legacy header "${legacy}"; migrate sender to "X-IQAuth-Signature" (back-compat removed in next minor).`
2953
+ );
2954
+ break;
2955
+ }
2956
+ }
2957
+ }
2958
+ if (!headerValue) {
2959
+ throw new WebhookSignatureError(
2960
+ "MISSING_HEADER",
2961
+ `Missing webhook signature header. Expected "X-IQAuth-Signature" (or one of: ${LEGACY_SIGNATURE_HEADERS.join(", ")}).`
2962
+ );
2963
+ }
2964
+ const { t, v1 } = parseHeader(headerValue);
2965
+ if (v1.length === 0) {
2966
+ throw new WebhookSignatureError(
2967
+ "MALFORMED_HEADER",
2968
+ `Could not parse "${usedHeader}" header value: ${headerValue}`
2969
+ );
2970
+ }
2971
+ const body = toBuffer(rawBody);
2972
+ let verifiedIdx = -1;
2973
+ for (let i = 0; i < secrets.length; i++) {
2974
+ const secret = secrets[i];
2975
+ if (!secret) continue;
2976
+ const { modern, legacy } = computeSignatures(secret, body, t);
2977
+ const ok = v1.some((sig) => {
2978
+ const lower = sig.toLowerCase();
2979
+ if (timingSafeEqualHex(lower, modern)) return true;
2980
+ if (legacy && timingSafeEqualHex(lower, legacy)) return true;
2981
+ return false;
2982
+ });
2983
+ if (ok) {
2984
+ verifiedIdx = i;
2985
+ break;
2986
+ }
2987
+ }
2988
+ if (verifiedIdx === -1) {
2989
+ throw new WebhookSignatureError(
2990
+ "SIGNATURE_MISMATCH",
2991
+ "Webhook signature does not match any provided secret"
2992
+ );
2993
+ }
2994
+ let parsed;
2995
+ try {
2996
+ parsed = JSON.parse(body.toString("utf8"));
2997
+ } catch {
2998
+ throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
2999
+ }
3000
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3001
+ envelopeError("Webhook body must be a JSON object");
3002
+ }
3003
+ const { id, type, subject, time, data, tenantId, specversion } = parsed;
3004
+ if (specversion !== "1.0") {
3005
+ envelopeError(`Envelope \`specversion\` must be "1.0" (got: ${JSON.stringify(specversion)})`);
3006
+ }
3007
+ if (typeof id !== "string" || !id) envelopeError("Envelope missing required string `id`");
3008
+ if (typeof type !== "string" || !type) envelopeError("Envelope missing required string `type`");
3009
+ if (typeof subject !== "string" || !subject) envelopeError("Envelope missing required string `subject`");
3010
+ if (typeof time !== "string" || !time) envelopeError("Envelope missing required string `time`");
3011
+ if (typeof tenantId !== "string" || !tenantId) envelopeError("Envelope missing required string `tenantId`");
3012
+ if (data === void 0 || data === null || typeof data !== "object" || Array.isArray(data)) {
3013
+ envelopeError("Envelope `data` must be an object");
3014
+ }
3015
+ const tolerance = opts.toleranceSeconds ?? 300;
3016
+ const eventMs = Date.parse(time);
3017
+ if (!Number.isFinite(eventMs)) envelopeError(`Envelope \`time\` is not a valid ISO timestamp: ${time}`);
3018
+ const nowMs = opts.nowMs ?? Date.now();
3019
+ if (Math.abs(nowMs - eventMs) > tolerance * 1e3) {
3020
+ throw new WebhookSignatureError(
3021
+ "TIMESTAMP_OUT_OF_TOLERANCE",
3022
+ `Envelope time ${time} is outside the ${tolerance}s tolerance window (now=${new Date(nowMs).toISOString()})`
3023
+ );
3024
+ }
3025
+ return {
3026
+ specversion: "1.0",
3027
+ id,
3028
+ type,
3029
+ subject,
3030
+ time,
3031
+ tenantId,
3032
+ data,
3033
+ idempotencyKey: id,
3034
+ verifiedWithSecretIndex: verifiedIdx
3035
+ };
3036
+ }
2549
3037
 
2550
3038
  // src/server/provisioningBridge.ts
2551
3039
  function defaultIsUniqueViolation(err) {
@@ -2611,10 +3099,13 @@ function createProvisioningBridge(options) {
2611
3099
  ErrorCodes,
2612
3100
  GdprModule,
2613
3101
  HierarchyModule,
3102
+ IQAUTH_SIGNATURE_HEADER,
2614
3103
  IQAuthClient,
2615
3104
  IQAuthError,
3105
+ IQ_AUTH_ERROR_CODES,
2616
3106
  InMemoryOidcStateStore,
2617
3107
  InvitesModule,
3108
+ LEGACY_SIGNATURE_HEADERS,
2618
3109
  MembershipsModule,
2619
3110
  MfaModule,
2620
3111
  OidcModule,
@@ -2632,14 +3123,19 @@ function createProvisioningBridge(options) {
2632
3123
  WebhookSignatureError,
2633
3124
  WebhooksModule,
2634
3125
  assertPublishableKey,
3126
+ buildUserinfoResponse,
2635
3127
  createProvisioningBridge,
2636
3128
  createTestIssuer,
2637
3129
  encodePublishableKey,
3130
+ expandPermissions,
3131
+ handleUserinfo,
3132
+ hasPermission,
2638
3133
  iqAuthMiddleware,
2639
3134
  isPublishableKey,
2640
3135
  isSecretKey,
2641
3136
  isValidWebhookSignature,
2642
3137
  parsePublishableKey,
3138
+ parseWebhookEvent,
2643
3139
  verifyWebhookSignature,
2644
3140
  verifyWsUpgrade
2645
3141
  });