@iqauth/sdk 2.6.4 → 2.8.1

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 (117) 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 +212 -46
  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 +293 -34
  9. package/dist/browser.mjs +5 -5
  10. package/dist/{chunk-BVV54LPI.mjs → chunk-25SSYDIP.mjs} +10 -4
  11. package/dist/{chunk-XAWYUPMO.mjs → chunk-4V7FKOTG.mjs} +242 -22
  12. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  13. package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
  14. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  15. package/dist/chunk-GLXSIGVS.mjs +66 -0
  16. package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
  17. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  18. package/dist/chunk-JRDVUWAL.mjs +46 -0
  19. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  20. package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
  21. package/dist/chunk-VYQ3ETCK.mjs +244 -0
  22. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  23. package/dist/chunk-WHT6WKTY.mjs +3180 -0
  24. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  25. package/dist/chunk-WSH4SW7F.mjs +490 -0
  26. package/dist/{chunk-W3F4JYGP.mjs → chunk-ZLJPABB7.mjs} +139 -23
  27. package/dist/cli/index.js +2 -2
  28. package/dist/cli/index.mjs +2 -2
  29. package/dist/{client-BNQe3AgF.d.ts → client-D8L-PaWr.d.mts} +59 -6
  30. package/dist/{client-kYlJFgPv.d.mts → client-DkPL0EPZ.d.ts} +59 -6
  31. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  32. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  33. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  34. package/dist/{express-CHpfa7D_.d.ts → express-Budysq4h.d.ts} +2 -2
  35. package/dist/{express-B6_1vBYZ.d.mts → express-DDTA3qV1.d.mts} +2 -2
  36. package/dist/express.d.mts +7 -6
  37. package/dist/express.d.ts +7 -6
  38. package/dist/express.js +563 -85
  39. package/dist/express.mjs +73 -34
  40. package/dist/fastify.d.mts +10 -0
  41. package/dist/fastify.d.ts +10 -0
  42. package/dist/fastify.js +589 -65
  43. package/dist/fastify.mjs +101 -11
  44. package/dist/hono.d.mts +10 -0
  45. package/dist/hono.d.ts +10 -0
  46. package/dist/hono.js +566 -65
  47. package/dist/hono.mjs +78 -11
  48. package/dist/index-Cko-d5po.d.mts +1848 -0
  49. package/dist/index-RNqwEcmY.d.ts +1848 -0
  50. package/dist/index.d.mts +56 -8
  51. package/dist/index.d.ts +56 -8
  52. package/dist/index.js +694 -75
  53. package/dist/index.mjs +30 -10
  54. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  55. package/dist/locales.d.mts +1 -1
  56. package/dist/locales.d.ts +1 -1
  57. package/dist/locales.js +36 -0
  58. package/dist/locales.mjs +1 -1
  59. package/dist/mobile.d.mts +77 -7
  60. package/dist/mobile.d.ts +77 -7
  61. package/dist/mobile.js +307 -46
  62. package/dist/mobile.mjs +98 -3
  63. package/dist/next.d.mts +10 -1
  64. package/dist/next.d.ts +10 -1
  65. package/dist/next.js +596 -205
  66. package/dist/next.mjs +83 -10
  67. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
  68. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
  69. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  70. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  71. package/dist/react-permissions.d.mts +52 -0
  72. package/dist/react-permissions.d.ts +52 -0
  73. package/dist/react-permissions.js +239 -0
  74. package/dist/react-permissions.mjs +98 -0
  75. package/dist/react.d.mts +9 -1624
  76. package/dist/react.d.ts +9 -1624
  77. package/dist/react.js +882 -73
  78. package/dist/react.mjs +71 -2631
  79. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  80. package/dist/server/handlers.d.mts +200 -4
  81. package/dist/server/handlers.d.ts +200 -4
  82. package/dist/server/handlers.js +530 -16
  83. package/dist/server/handlers.mjs +14 -3
  84. package/dist/server.d.mts +171 -8
  85. package/dist/server.d.ts +171 -8
  86. package/dist/server.js +579 -61
  87. package/dist/server.mjs +99 -12
  88. package/dist/service.d.mts +4 -4
  89. package/dist/service.d.ts +4 -4
  90. package/dist/service.js +212 -46
  91. package/dist/service.mjs +3 -3
  92. package/dist/{signIn-CiIBTJIh.d.mts → signIn-CReqfXsh.d.mts} +95 -3
  93. package/dist/{signIn-OCr88Zf8.d.ts → signIn-Cfa1GTpO.d.ts} +95 -3
  94. package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
  95. package/dist/test.mjs +3 -3
  96. package/dist/{tokens-DCyzzn8L.d.mts → tokens-9F6ETrzk.d.ts} +9 -2
  97. package/dist/{tokens-aHiGFr_E.d.ts → tokens-B06VtvUi.d.mts} +9 -2
  98. package/dist/{types-DZAflmmq.d.mts → types-Bn8O-OEd.d.mts} +164 -11
  99. package/dist/{types-DZAflmmq.d.ts → types-Bn8O-OEd.d.ts} +164 -11
  100. package/dist/{types-6bNdxesb.d.ts → types-DnU2LhXR.d.mts} +7 -1
  101. package/dist/{types-6bNdxesb.d.mts → types-DnU2LhXR.d.ts} +7 -1
  102. package/dist/webhooks.d.mts +113 -17
  103. package/dist/webhooks.d.ts +113 -17
  104. package/dist/webhooks.js +179 -15
  105. package/dist/webhooks.mjs +7 -1
  106. package/dist/ws.d.mts +2 -2
  107. package/dist/ws.d.ts +2 -2
  108. package/dist/ws.js +80 -30
  109. package/dist/ws.mjs +4 -4
  110. package/docs/error-handling.md +101 -0
  111. package/docs/guides/effective-permissions.md +171 -0
  112. package/docs/guides/invitations.md +65 -0
  113. package/package.json +19 -4
  114. package/dist/chunk-6TDJJER7.mjs +0 -217
  115. package/dist/chunk-UKZLOHZG.mjs +0 -83
  116. package/dist/errors-CDdl24MP.d.mts +0 -52
  117. 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}`;
@@ -356,17 +393,27 @@ function parseLoginResponse(data, browserSessionMode) {
356
393
  tenants: data.tenants
357
394
  };
358
395
  }
396
+ if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
397
+ return {
398
+ status: "scope_selection",
399
+ scopeSelectionToken: data.scopeSelectionToken,
400
+ tenantId: data.tenantId,
401
+ scopes: data.scopes
402
+ };
403
+ }
359
404
  throw new Error("Unexpected login response shape");
360
405
  }
361
406
  var AuthModule = class {
362
407
  constructor(http) {
363
408
  this.http = http;
364
409
  }
365
- async login(email, password) {
410
+ async login(email, password, opts) {
411
+ const body = { email, password };
412
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
366
413
  const data = await this.http.request(
367
414
  "POST",
368
415
  "/api/v1/auth/login",
369
- { email, password },
416
+ body,
370
417
  { skipAutoRefresh: true }
371
418
  );
372
419
  return parseLoginResponse(data, this.http.isBrowserSession());
@@ -404,13 +451,29 @@ var AuthModule = class {
404
451
  method
405
452
  }, { skipAutoRefresh: true });
406
453
  }
407
- async selectTenant(tenantSelectionToken, tenantId) {
454
+ async selectTenant(tenantSelectionToken, tenantId, opts) {
455
+ const body = { tenantSelectionToken, tenantId };
456
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
408
457
  const data = await this.http.request(
409
458
  "POST",
410
459
  "/api/v1/auth/select-tenant",
460
+ body,
461
+ { skipAutoRefresh: true }
462
+ );
463
+ return parseLoginResponse(data, this.http.isBrowserSession());
464
+ }
465
+ /**
466
+ * Task #171 — redeem a scope-selection token + chosen membership for a
467
+ * real authenticated session. `membershipId` must be one of the scopes
468
+ * returned in the prior `scope_selection` envelope.
469
+ */
470
+ async selectScope(scopeSelectionToken, membershipId) {
471
+ const data = await this.http.request(
472
+ "POST",
473
+ "/api/v1/auth/select-scope",
411
474
  {
412
- tenantSelectionToken,
413
- tenantId
475
+ scopeSelectionToken,
476
+ membershipId
414
477
  },
415
478
  { skipAutoRefresh: true }
416
479
  );
@@ -497,6 +560,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
497
560
  "iqvalidate"
498
561
  ];
499
562
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
563
+ function classifyJoseError(err) {
564
+ if (err instanceof import_jose.errors.JWTExpired) {
565
+ return { code: "token_expired", message: "Token has expired" };
566
+ }
567
+ if (err instanceof import_jose.errors.JOSEError) {
568
+ return { code: "token_invalid", message: err.message };
569
+ }
570
+ if (err instanceof Error) {
571
+ return { code: "token_invalid", message: err.message };
572
+ }
573
+ return { code: "token_invalid", message: "Token verification failed" };
574
+ }
500
575
  function decodeProtectedHeader(token) {
501
576
  const parts = token.split(".");
502
577
  if (parts.length < 2) return null;
@@ -533,11 +608,11 @@ var TokensModule = class {
533
608
  async verify(token, options = {}) {
534
609
  const header = decodeProtectedHeader(token);
535
610
  if (!header) {
536
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
611
+ throw new IQAuthError("token_invalid", "Unable to decode token");
537
612
  }
538
613
  const kid = header.kid;
539
614
  if (!kid) {
540
- throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
615
+ throw new IQAuthError("token_invalid", "Token missing kid header");
541
616
  }
542
617
  let cache = await this.ensureCache();
543
618
  if (!cache.byKid.has(kid)) {
@@ -545,7 +620,7 @@ var TokensModule = class {
545
620
  cache = await this.ensureCache();
546
621
  }
547
622
  if (!cache.byKid.has(kid)) {
548
- throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
623
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
549
624
  }
550
625
  const issuer = options.issuer ?? this.defaultIssuer;
551
626
  const audience = options.audience ?? this.defaultAudience;
@@ -561,16 +636,8 @@ var TokensModule = class {
561
636
  const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
562
637
  return payload;
563
638
  } 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");
639
+ const classified = classifyJoseError(err);
640
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
574
641
  }
575
642
  }
576
643
  /**
@@ -612,7 +679,7 @@ var TokensModule = class {
612
679
  getClaims(token) {
613
680
  const claims = this.decode(token);
614
681
  if (!claims) {
615
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
682
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
616
683
  }
617
684
  return claims;
618
685
  }
@@ -622,7 +689,7 @@ var TokensModule = class {
622
689
  }
623
690
  await this.refreshJwks();
624
691
  if (!this.jwksCache) {
625
- throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
692
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
626
693
  }
627
694
  return this.jwksCache;
628
695
  }
@@ -632,22 +699,38 @@ var TokensModule = class {
632
699
  }
633
700
  this.inFlightRefresh = (async () => {
634
701
  try {
635
- const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
702
+ let res;
703
+ try {
704
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
705
+ } catch (err) {
706
+ throw new IQAuthError(
707
+ "network",
708
+ err instanceof Error ? err.message : "JWKS fetch network error",
709
+ void 0,
710
+ err
711
+ );
712
+ }
636
713
  if (!res.ok) {
637
714
  throw new IQAuthError(
638
- "INTERNAL_ERROR",
639
- `Failed to fetch JWKS: ${res.status}`
715
+ "jwks_fetch_failed",
716
+ `Failed to fetch JWKS: ${res.status}`,
717
+ res.status
640
718
  );
641
719
  }
642
720
  let jwks;
643
721
  try {
644
722
  jwks = await res.json();
645
- } catch {
646
- throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
723
+ } catch (err) {
724
+ throw new IQAuthError(
725
+ "jwks_fetch_failed",
726
+ "Malformed JWKS response: invalid JSON",
727
+ res.status,
728
+ err
729
+ );
647
730
  }
648
731
  if (!jwks || !Array.isArray(jwks.keys)) {
649
732
  throw new IQAuthError(
650
- "INTERNAL_ERROR",
733
+ "jwks_fetch_failed",
651
734
  "Malformed JWKS response: expected { keys: [...] }"
652
735
  );
653
736
  }
@@ -655,7 +738,7 @@ var TokensModule = class {
655
738
  for (const key of jwks.keys) {
656
739
  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
740
  throw new IQAuthError(
658
- "INTERNAL_ERROR",
741
+ "jwks_fetch_failed",
659
742
  "Malformed JWKS response: key missing required fields"
660
743
  );
661
744
  }
@@ -673,6 +756,19 @@ var TokensModule = class {
673
756
  clearCache() {
674
757
  this.jwksCache = null;
675
758
  }
759
+ /**
760
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
761
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
762
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
763
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
764
+ */
765
+ async prewarm() {
766
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
767
+ try {
768
+ await this.refreshJwks();
769
+ } catch {
770
+ }
771
+ }
676
772
  };
677
773
 
678
774
  // src/modules/sessions.ts
@@ -996,14 +1092,14 @@ var OidcModule = class {
996
1092
  */
997
1093
  async handleCallback(params) {
998
1094
  if (!params.state) {
999
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing state parameter");
1095
+ throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
1000
1096
  }
1001
1097
  if (!params.code) {
1002
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing code parameter");
1098
+ throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
1003
1099
  }
1004
1100
  const stored = await this.stateStore.get(params.state);
1005
1101
  if (!stored) {
1006
- throw new IQAuthError("VALIDATION_ERROR", "Unknown or expired OIDC state");
1102
+ throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
1007
1103
  }
1008
1104
  let tokens;
1009
1105
  try {
@@ -1021,7 +1117,7 @@ var OidcModule = class {
1021
1117
  if (tokens.id_token) {
1022
1118
  if (!this.tokensModule) {
1023
1119
  throw new IQAuthError(
1024
- "INTERNAL_ERROR",
1120
+ "config_invalid",
1025
1121
  "OIDC handleCallback received an id_token but no TokensModule is configured for verification"
1026
1122
  );
1027
1123
  }
@@ -1032,7 +1128,7 @@ var OidcModule = class {
1032
1128
  const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
1033
1129
  if (!tokenNonce || tokenNonce !== stored.nonce) {
1034
1130
  throw new IQAuthError(
1035
- "TOKEN_INVALID",
1131
+ "token_invalid",
1036
1132
  "OIDC id_token nonce did not match the stored value"
1037
1133
  );
1038
1134
  }
@@ -1233,6 +1329,9 @@ var AppsModule = class {
1233
1329
  * @remarks Wraps GET /api/v1/apps/:appKey
1234
1330
  */
1235
1331
  async get(appKey) {
1332
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1333
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1334
+ }
1236
1335
  return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
1237
1336
  }
1238
1337
  /**
@@ -1252,6 +1351,16 @@ var AppsModule = class {
1252
1351
  401
1253
1352
  );
1254
1353
  }
1354
+ if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
1355
+ throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
1356
+ }
1357
+ if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
1358
+ throw new IQAuthError(
1359
+ "ENVIRONMENT_REQUIRED",
1360
+ "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.",
1361
+ 400
1362
+ );
1363
+ }
1255
1364
  return this.http.request("POST", "/api/v1/apps/sync", manifest);
1256
1365
  }
1257
1366
  /**
@@ -1261,11 +1370,14 @@ var AppsModule = class {
1261
1370
  * @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
1262
1371
  */
1263
1372
  async isRegistered(appKey) {
1373
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1374
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1375
+ }
1264
1376
  try {
1265
1377
  await this.get(appKey);
1266
1378
  return true;
1267
1379
  } catch (err) {
1268
- if (err.code === "NOT_FOUND" || err.status === 404) {
1380
+ if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
1269
1381
  return false;
1270
1382
  }
1271
1383
  throw err;
@@ -1302,6 +1414,20 @@ var RolesModule = class {
1302
1414
  };
1303
1415
 
1304
1416
  // src/modules/permissionGroups.ts
1417
+ function assertAppKey(appKey, callsite) {
1418
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1419
+ throw new IQAuthError(
1420
+ "VALIDATION_ERROR",
1421
+ `appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
1422
+ 400
1423
+ );
1424
+ }
1425
+ }
1426
+ function assertNodeKey(nodeKey, callsite) {
1427
+ if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
1428
+ throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
1429
+ }
1430
+ }
1305
1431
  var PermissionGroupsModule = class {
1306
1432
  constructor(http) {
1307
1433
  this.http = http;
@@ -1322,7 +1448,14 @@ var PermissionGroupsModule = class {
1322
1448
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
1323
1449
  }
1324
1450
  async addPermission(tenantId, groupId, data) {
1325
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, data);
1451
+ assertAppKey(data?.appKey, "permissionGroups.addPermission");
1452
+ assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
1453
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
1454
+ appKey: data.appKey,
1455
+ nodeKey: data.nodeKey,
1456
+ effect: data.effect,
1457
+ weight: data.weight
1458
+ });
1326
1459
  }
1327
1460
  async removePermission(tenantId, groupId, permissionId) {
1328
1461
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
@@ -1346,21 +1479,51 @@ var PermissionGroupsModule = class {
1346
1479
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
1347
1480
  }
1348
1481
  async addUserOverride(tenantId, userId, data) {
1349
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, data);
1482
+ assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
1483
+ assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
1484
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
1485
+ appKey: data.appKey,
1486
+ nodeKey: data.nodeKey,
1487
+ effect: data.effect,
1488
+ weight: data.weight,
1489
+ expiresAt: data.expiresAt
1490
+ });
1350
1491
  }
1351
1492
  async removeUserOverride(tenantId, userId, overrideId) {
1352
1493
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
1353
1494
  }
1495
+ /**
1496
+ * Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
1497
+ * longer accepted at the SDK boundary; pass it as `appKey` instead. The
1498
+ * server still accepts `product=` from raw HTTP callers during the
1499
+ * deprecation window, but the SDK will not silently translate it.
1500
+ */
1354
1501
  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}` : ""}`);
1502
+ assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
1503
+ const qs = new URLSearchParams({ appKey: params.appKey }).toString();
1504
+ return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
1360
1505
  }
1361
1506
  async checkPermission(tenantId, userId, appKey, nodeKey) {
1507
+ assertAppKey(appKey, "permissionGroups.checkPermission");
1508
+ assertNodeKey(nodeKey, "permissionGroups.checkPermission");
1362
1509
  return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
1363
1510
  }
1511
+ /**
1512
+ * Task #130 — every entry in `checks` must include a non-empty `appKey`
1513
+ * AND `nodeKey`. The SDK validates the whole batch before sending so a
1514
+ * single misconfigured entry can't slip through and silently report
1515
+ * `allowed: false` from the server's per-entry validation branch.
1516
+ */
1517
+ async batchCheckPermissions(tenantId, userId, checks) {
1518
+ if (!Array.isArray(checks) || checks.length === 0) {
1519
+ throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
1520
+ }
1521
+ checks.forEach((c, i) => {
1522
+ assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
1523
+ assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
1524
+ });
1525
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
1526
+ }
1364
1527
  };
1365
1528
 
1366
1529
  // src/modules/apiKeys.ts
@@ -1785,6 +1948,10 @@ var IQAuthClient = class _IQAuthClient {
1785
1948
  this._refreshToken = tokens.refreshToken;
1786
1949
  },
1787
1950
  autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
1951
+ // `'app-state'` is mobile-only — on any other environment we treat it
1952
+ // as the default `true` (proactive refresh ON). Only the mobile client
1953
+ // disables proactive refresh and replaces it with an AppState listener.
1954
+ proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
1788
1955
  onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
1789
1956
  sessionHeaderName: config.sessionHeaderName,
1790
1957
  sessionHeaderValue: config.sessionHeaderValue,
@@ -1825,6 +1992,13 @@ var IQAuthClient = class _IQAuthClient {
1825
1992
  static forServer(config) {
1826
1993
  return new _IQAuthClient({ ...config, environment: "server" });
1827
1994
  }
1995
+ /**
1996
+ * Construct a mobile-environment client. NOTE: this constructor does NOT
1997
+ * subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
1998
+ * is passed — it only disables the per-request proactive refresh. Use
1999
+ * `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
2000
+ * AppState-driven refresh behavior (recommended for Expo / React Native).
2001
+ */
1828
2002
  static forMobile(config) {
1829
2003
  return new _IQAuthClient({ ...config, environment: "mobile" });
1830
2004
  }
@@ -1841,6 +2015,18 @@ var IQAuthClient = class _IQAuthClient {
1841
2015
  getRefreshToken() {
1842
2016
  return this._refreshToken;
1843
2017
  }
2018
+ /**
2019
+ * Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
2020
+ * refresh round-trip on the request hot path doesn't pay the discovery
2021
+ * fetch latency. Safe to call repeatedly. Errors are swallowed; callers
2022
+ * may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
2023
+ */
2024
+ async prewarm() {
2025
+ await Promise.all([
2026
+ this.tokens.prewarm(),
2027
+ this.oidc.getDiscovery().catch(() => void 0)
2028
+ ]);
2029
+ }
1844
2030
  getCurrentClaims() {
1845
2031
  if (!this._accessToken) return null;
1846
2032
  return this.tokens.decode(this._accessToken);
@@ -1911,14 +2097,14 @@ function assertPublishableKey(raw, opts) {
1911
2097
  const ctx = opts?.context ? `${opts.context}: ` : "";
1912
2098
  if (typeof raw !== "string" || raw.length === 0) {
1913
2099
  throw new IQAuthError(
1914
- "CONFIG_INVALID",
2100
+ "config_invalid",
1915
2101
  `${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
2102
  );
1917
2103
  }
1918
2104
  const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
1919
2105
  if (!shapeMatch) {
1920
2106
  throw new IQAuthError(
1921
- "CONFIG_INVALID",
2107
+ "config_invalid",
1922
2108
  `${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
2109
  );
1924
2110
  }
@@ -1927,19 +2113,19 @@ function assertPublishableKey(raw, opts) {
1927
2113
  decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
1928
2114
  } catch {
1929
2115
  throw new IQAuthError(
1930
- "CONFIG_INVALID",
2116
+ "config_invalid",
1931
2117
  `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
1932
2118
  );
1933
2119
  }
1934
2120
  if (!isPublishableKeyPayload(decoded)) {
1935
2121
  throw new IQAuthError(
1936
- "CONFIG_INVALID",
2122
+ "config_invalid",
1937
2123
  `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
1938
2124
  );
1939
2125
  }
1940
2126
  if (!isValidIssuerUrl(decoded.iss)) {
1941
2127
  throw new IQAuthError(
1942
- "CONFIG_INVALID",
2128
+ "config_invalid",
1943
2129
  `${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
2130
  );
1945
2131
  }
@@ -1959,12 +2145,18 @@ function isSecretKey(raw) {
1959
2145
 
1960
2146
  // src/middleware/express.ts
1961
2147
  var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
2148
+ // Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
1962
2149
  "TOKEN_INVALID",
1963
2150
  "TOKEN_EXPIRED",
1964
2151
  "TOKEN_REVOKED",
1965
2152
  "SESSION_EXPIRED",
1966
2153
  "SESSION_INVALID",
1967
- "AUTH_REQUIRED"
2154
+ "AUTH_REQUIRED",
2155
+ // Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
2156
+ // Mapped to 401 here so framework consumers don't have to learn the new
2157
+ // codes to keep their auth-failure handling working.
2158
+ "token_invalid",
2159
+ "token_expired"
1968
2160
  ]);
1969
2161
  var DEFAULT_ACCESS_COOKIE = "iqauth_at";
1970
2162
  function getAuthorizationHeader(req) {
@@ -2146,6 +2338,246 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
2146
2338
  };
2147
2339
  }
2148
2340
 
2341
+ // src/server/handlers.ts
2342
+ async function buildUserinfoResponse(claims, opts = {}) {
2343
+ const baseUser = {
2344
+ sub: claims.sub,
2345
+ email: claims.email,
2346
+ name: claims.name,
2347
+ tenantId: claims.tenantId,
2348
+ vendorId: claims.vendorId,
2349
+ roles: claims.roles ?? [],
2350
+ entitlements: claims.entitlements ?? [],
2351
+ // Task #171 — project the active source/client scope onto the userinfo
2352
+ // payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
2353
+ // expose it without consumers having to re-decode the JWT.
2354
+ ...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
2355
+ };
2356
+ const enriched = opts.enrich ? await opts.enrich(claims) : null;
2357
+ const user = enriched ? { ...baseUser, ...enriched } : baseUser;
2358
+ return {
2359
+ success: true,
2360
+ data: {
2361
+ user,
2362
+ claims,
2363
+ tenantId: claims.tenantId ?? null
2364
+ }
2365
+ };
2366
+ }
2367
+ var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
2368
+ var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
2369
+ function assertCookiePrefixInvariants(name, secure, path, domain) {
2370
+ if (name.startsWith("__Host-")) {
2371
+ if (!secure) {
2372
+ throw new IQAuthError(
2373
+ "config_invalid",
2374
+ `Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
2375
+ );
2376
+ }
2377
+ if (path !== "/") {
2378
+ throw new IQAuthError(
2379
+ "config_invalid",
2380
+ `Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
2381
+ );
2382
+ }
2383
+ if (domain) {
2384
+ throw new IQAuthError(
2385
+ "config_invalid",
2386
+ `Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
2387
+ );
2388
+ }
2389
+ } else if (name.startsWith("__Secure-") && !secure) {
2390
+ throw new IQAuthError(
2391
+ "config_invalid",
2392
+ `Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
2393
+ );
2394
+ }
2395
+ }
2396
+ function resolve(config) {
2397
+ const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
2398
+ const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
2399
+ maybeWarnDefaultSignoutRegistry(config);
2400
+ const secure = config.secure ?? true;
2401
+ if (config.secure === false && config.allowInsecureCookies !== true) {
2402
+ throw new IQAuthError(
2403
+ "config_invalid",
2404
+ "Refusing to issue auth cookies with secure:false \u2014 this exposes session cookies over plaintext HTTP. For local HTTP development, set allowInsecureCookies:true to acknowledge the risk. Production MUST use HTTPS with secure cookies."
2405
+ );
2406
+ }
2407
+ const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
2408
+ const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
2409
+ const stateCookieName = config.stateCookieName ?? "iqauth_state";
2410
+ const cookiePath = config.cookiePath ?? "/";
2411
+ const cookieDomain = config.cookieDomain;
2412
+ for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
2413
+ assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
2414
+ }
2415
+ return {
2416
+ publishableKey: config.publishableKey,
2417
+ secretKey: config.secretKey,
2418
+ issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
2419
+ accessCookieName,
2420
+ refreshCookieName,
2421
+ cookieDomain,
2422
+ sameSite: config.sameSite ?? "lax",
2423
+ secure,
2424
+ cookiePath,
2425
+ tokenPath: config.tokenPath ?? "/oidc/token",
2426
+ refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
2427
+ logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
2428
+ fetchImpl: config.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
2429
+ throw new Error("global fetch is unavailable; pass fetchImpl");
2430
+ })),
2431
+ appId: parsed.appId,
2432
+ tenantId: parsed.tenantId,
2433
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
2434
+ debug: config.debug,
2435
+ onTimingEvent: config.onTimingEvent,
2436
+ signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
2437
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
2438
+ requireOAuthState: config.requireOAuthState ?? true,
2439
+ stateCookieName: config.stateCookieName ?? "iqauth_state"
2440
+ };
2441
+ }
2442
+ var DEFAULT_SIGNOUT_TTL_MS = 6e4;
2443
+ var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
2444
+ function pruneInMemoryMarkers(now) {
2445
+ if (inMemorySignoutMarkers.size === 0) return;
2446
+ for (const [k, exp] of inMemorySignoutMarkers) {
2447
+ if (exp <= now) inMemorySignoutMarkers.delete(k);
2448
+ }
2449
+ }
2450
+ var defaultSignoutRegistry = {
2451
+ mark(token, ttlMs) {
2452
+ const now = Date.now();
2453
+ pruneInMemoryMarkers(now);
2454
+ inMemorySignoutMarkers.set(token, now + ttlMs);
2455
+ },
2456
+ has(token) {
2457
+ const now = Date.now();
2458
+ const exp = inMemorySignoutMarkers.get(token);
2459
+ if (!exp) return false;
2460
+ if (exp <= now) {
2461
+ inMemorySignoutMarkers.delete(token);
2462
+ return false;
2463
+ }
2464
+ return true;
2465
+ }
2466
+ };
2467
+ var warnedDefaultSignoutRegistry = false;
2468
+ function maybeWarnDefaultSignoutRegistry(config) {
2469
+ if (warnedDefaultSignoutRegistry) return;
2470
+ if (config.signoutRegistry) return;
2471
+ warnedDefaultSignoutRegistry = true;
2472
+ console.warn(
2473
+ "[IQAuth] Using the in-memory signout registry (process-local). Signout idempotency is NOT shared across instances \u2014 in a multi-replica deployment a /refresh racing a /signout on another replica can reissue cookies after sign-out. Plug a shared backend (e.g. Redis) into IQAuthHelperConfig.signoutRegistry to fix this and silence this warning."
2474
+ );
2475
+ }
2476
+ var TOKENS_CACHE = /* @__PURE__ */ new Map();
2477
+ function getTokensFor(issuer) {
2478
+ let m = TOKENS_CACHE.get(issuer);
2479
+ if (!m) {
2480
+ m = new TokensModule(issuer);
2481
+ TOKENS_CACHE.set(issuer, m);
2482
+ }
2483
+ return m;
2484
+ }
2485
+ async function handleUserinfo(config, input) {
2486
+ const cfg = resolve(config);
2487
+ if (!input.accessToken) {
2488
+ return {
2489
+ status: 401,
2490
+ body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
2491
+ cookies: []
2492
+ };
2493
+ }
2494
+ let claims;
2495
+ try {
2496
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
2497
+ issuer: cfg.issuer,
2498
+ ...config.verify
2499
+ });
2500
+ } catch (err) {
2501
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2502
+ const message = err instanceof Error ? err.message : "Access token verification failed";
2503
+ return {
2504
+ status: 401,
2505
+ body: { success: false, error: { code, message } },
2506
+ cookies: []
2507
+ };
2508
+ }
2509
+ const envelope = await buildUserinfoResponse(claims, {
2510
+ enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
2511
+ });
2512
+ return {
2513
+ status: 200,
2514
+ body: envelope,
2515
+ cookies: []
2516
+ };
2517
+ }
2518
+
2519
+ // src/permissions/wildcard.ts
2520
+ var SUFFIX = ".*";
2521
+ function wildcardPrefix(pattern) {
2522
+ return pattern.slice(0, -SUFFIX.length);
2523
+ }
2524
+ function hasPermission(set, id) {
2525
+ if (!id) return false;
2526
+ if (!set) return false;
2527
+ if (id === "*") {
2528
+ for (const entry of set) if (entry === "*") return true;
2529
+ return false;
2530
+ }
2531
+ const queryIsWildcard = id.endsWith(SUFFIX);
2532
+ const queryPrefix = queryIsWildcard ? wildcardPrefix(id) : null;
2533
+ for (const entry of set) {
2534
+ if (!entry) continue;
2535
+ if (entry === "*") return true;
2536
+ if (entry === id) return true;
2537
+ if (entry.endsWith(SUFFIX)) {
2538
+ const prefix = wildcardPrefix(entry);
2539
+ if (!queryIsWildcard) {
2540
+ if (id === prefix) return true;
2541
+ if (id.startsWith(prefix + ".")) return true;
2542
+ } else {
2543
+ if (queryPrefix === prefix) return true;
2544
+ if (queryPrefix !== null && queryPrefix.startsWith(prefix + ".")) return true;
2545
+ }
2546
+ }
2547
+ }
2548
+ return false;
2549
+ }
2550
+ function expandPermissions(set) {
2551
+ if (!set) return [];
2552
+ const seen = /* @__PURE__ */ new Set();
2553
+ for (const raw of set) {
2554
+ if (typeof raw !== "string" || raw.length === 0) continue;
2555
+ seen.add(raw);
2556
+ }
2557
+ if (seen.has("*")) return ["*"];
2558
+ const wildcards = [];
2559
+ for (const entry of seen) if (entry.endsWith(SUFFIX)) wildcards.push(entry);
2560
+ const out = [];
2561
+ for (const entry of seen) {
2562
+ let covered = false;
2563
+ for (const w of wildcards) {
2564
+ if (w === entry) continue;
2565
+ const prefix = wildcardPrefix(w);
2566
+ if (entry === prefix) {
2567
+ covered = true;
2568
+ break;
2569
+ }
2570
+ if (entry.startsWith(prefix + ".")) {
2571
+ covered = true;
2572
+ break;
2573
+ }
2574
+ }
2575
+ if (!covered) out.push(entry);
2576
+ }
2577
+ out.sort();
2578
+ return out;
2579
+ }
2580
+
2149
2581
  // src/ws.ts
2150
2582
  var DEFAULT_COOKIE = "iqauth_at";
2151
2583
  var DEFAULT_SUBPROTOCOL_PREFIX = "iqauth.bearer.";
@@ -2244,10 +2676,10 @@ function jwkFromPublicKey(publicKey, kid) {
2244
2676
  return { kty: "RSA", use: "sig", alg: "RS256", kid, n: jwk.n, e: jwk.e };
2245
2677
  }
2246
2678
  function readBody(req) {
2247
- return new Promise((resolve, reject) => {
2679
+ return new Promise((resolve2, reject) => {
2248
2680
  const chunks = [];
2249
2681
  req.on("data", (c) => chunks.push(c));
2250
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
2682
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
2251
2683
  req.on("error", reject);
2252
2684
  });
2253
2685
  }
@@ -2435,11 +2867,11 @@ async function createTestIssuer(options = {}) {
2435
2867
  const server = (0, import_http2.createServer)((req, res) => {
2436
2868
  void handler(req, res);
2437
2869
  });
2438
- await new Promise((resolve, reject) => {
2870
+ await new Promise((resolve2, reject) => {
2439
2871
  server.once("error", reject);
2440
2872
  server.listen(port, host, () => {
2441
2873
  server.off("error", reject);
2442
- resolve();
2874
+ resolve2();
2443
2875
  });
2444
2876
  });
2445
2877
  const addr = server.address();
@@ -2463,8 +2895,8 @@ async function createTestIssuer(options = {}) {
2463
2895
  pendingCodes.set(code, { claims: opts, refreshToken });
2464
2896
  return code;
2465
2897
  },
2466
- close: () => new Promise((resolve, reject) => {
2467
- server.close((err) => err ? reject(err) : resolve());
2898
+ close: () => new Promise((resolve2, reject) => {
2899
+ server.close((err) => err ? reject(err) : resolve2());
2468
2900
  })
2469
2901
  };
2470
2902
  }
@@ -2478,6 +2910,12 @@ var WebhookSignatureError = class extends Error {
2478
2910
  this.code = code;
2479
2911
  }
2480
2912
  };
2913
+ var IQAUTH_SIGNATURE_HEADER = "x-iqauth-signature";
2914
+ var LEGACY_SIGNATURE_HEADERS = [
2915
+ "x-webhook-signature",
2916
+ "x-iq-auth-signature",
2917
+ "x-signature"
2918
+ ];
2481
2919
  function toBuffer(p) {
2482
2920
  if (typeof p === "string") return Buffer.from(p, "utf8");
2483
2921
  if (Buffer.isBuffer(p)) return p;
@@ -2486,13 +2924,19 @@ function toBuffer(p) {
2486
2924
  function parseHeader(header) {
2487
2925
  let t = NaN;
2488
2926
  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();
2927
+ const trimmed = header.trim();
2928
+ if (/^[0-9a-f]+$/i.test(trimmed)) {
2929
+ v1.push(trimmed.toLowerCase());
2930
+ return { t, v1 };
2931
+ }
2932
+ for (const part of trimmed.split(",")) {
2933
+ const eqIdx = part.indexOf("=");
2934
+ if (eqIdx === -1) continue;
2935
+ const key = part.slice(0, eqIdx).trim().toLowerCase();
2936
+ const value = part.slice(eqIdx + 1).trim();
2937
+ if (!value) continue;
2494
2938
  if (key === "t") t = Number(value);
2495
- else if (key === "v1") v1.push(value);
2939
+ else if (key === "v1") v1.push(value.toLowerCase());
2496
2940
  }
2497
2941
  return { t, v1 };
2498
2942
  }
@@ -2504,6 +2948,11 @@ function timingSafeEqualHex(a, b) {
2504
2948
  return false;
2505
2949
  }
2506
2950
  }
2951
+ function computeSignatures(secret, body, t) {
2952
+ const modern = import_crypto3.default.createHmac("sha256", secret).update(body).digest("hex");
2953
+ const legacy = Number.isFinite(t) ? import_crypto3.default.createHmac("sha256", secret).update(`${t}.`).update(body).digest("hex") : null;
2954
+ return { modern, legacy };
2955
+ }
2507
2956
  function verifyWebhookSignature(opts) {
2508
2957
  const headerRaw = Array.isArray(opts.header) ? opts.header[0] : opts.header;
2509
2958
  if (!headerRaw || typeof headerRaw !== "string") {
@@ -2513,20 +2962,23 @@ function verifyWebhookSignature(opts) {
2513
2962
  throw new WebhookSignatureError("MISSING_SECRET", "secret is required");
2514
2963
  }
2515
2964
  const { t, v1 } = parseHeader(headerRaw);
2516
- if (!Number.isFinite(t) || v1.length === 0) {
2965
+ if (v1.length === 0) {
2517
2966
  throw new WebhookSignatureError("MALFORMED_HEADER", `Could not parse signature header: ${headerRaw}`);
2518
2967
  }
2519
2968
  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
- );
2969
+ if (Number.isFinite(t)) {
2970
+ const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
2971
+ if (Math.abs(now - t) > tolerance) {
2972
+ throw new WebhookSignatureError(
2973
+ "TIMESTAMP_OUT_OF_TOLERANCE",
2974
+ `Signature timestamp ${t} is outside the ${tolerance}s tolerance window (now=${now})`
2975
+ );
2976
+ }
2526
2977
  }
2527
2978
  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));
2979
+ const { modern, legacy } = computeSignatures(opts.secret, body, t);
2980
+ const expected = Number.isFinite(t) ? legacy : modern;
2981
+ const matched = expected !== null && v1.some((sig) => timingSafeEqualHex(sig.toLowerCase(), expected));
2530
2982
  if (!matched) {
2531
2983
  throw new WebhookSignatureError("SIGNATURE_MISMATCH", "Webhook signature does not match expected value");
2532
2984
  }
@@ -2536,6 +2988,29 @@ function verifyWebhookSignature(opts) {
2536
2988
  } catch {
2537
2989
  throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
2538
2990
  }
2991
+ if (!Number.isFinite(t) && !opts.allowMissingTimestamp) {
2992
+ const rawTime = parsed.time;
2993
+ if (typeof rawTime !== "string" || !rawTime) {
2994
+ throw new WebhookSignatureError(
2995
+ "MISSING_TIMESTAMP",
2996
+ "Modern webhook delivery has no header `t=` and no envelope `time`; cannot enforce replay protection. Use parseWebhookEvent, or set allowMissingTimestamp:true (insecure)."
2997
+ );
2998
+ }
2999
+ const eventMs = Date.parse(rawTime);
3000
+ if (!Number.isFinite(eventMs)) {
3001
+ throw new WebhookSignatureError(
3002
+ "MALFORMED_TIMESTAMP",
3003
+ `Envelope \`time\` is not a valid ISO timestamp: ${rawTime}`
3004
+ );
3005
+ }
3006
+ const nowMs = (opts.nowSeconds ?? Math.floor(Date.now() / 1e3)) * 1e3;
3007
+ if (Math.abs(nowMs - eventMs) > tolerance * 1e3) {
3008
+ throw new WebhookSignatureError(
3009
+ "TIMESTAMP_OUT_OF_TOLERANCE",
3010
+ `Envelope time ${rawTime} is outside the ${tolerance}s tolerance window`
3011
+ );
3012
+ }
3013
+ }
2539
3014
  return parsed;
2540
3015
  }
2541
3016
  function isValidWebhookSignature(opts) {
@@ -2546,8 +3021,130 @@ function isValidWebhookSignature(opts) {
2546
3021
  return false;
2547
3022
  }
2548
3023
  }
3024
+ function readHeader(headers, name) {
3025
+ if (typeof headers.get === "function") {
3026
+ return headers.get(name);
3027
+ }
3028
+ const lower = name.toLowerCase();
3029
+ const obj = headers;
3030
+ if (lower in obj) return obj[lower];
3031
+ for (const [k, v] of Object.entries(obj)) {
3032
+ if (k.toLowerCase() === lower) return v;
3033
+ }
3034
+ return void 0;
3035
+ }
3036
+ function pickHeaderValue(value) {
3037
+ if (value == null) return null;
3038
+ if (Array.isArray(value)) return value[0] ?? null;
3039
+ return value;
3040
+ }
3041
+ function envelopeError(message) {
3042
+ throw new WebhookSignatureError("MALFORMED_ENVELOPE", message);
3043
+ }
3044
+ function parseWebhookEvent(rawBody, headers, secrets, opts = {}) {
3045
+ if (!Array.isArray(secrets) || secrets.length === 0 || secrets.every((s) => !s)) {
3046
+ throw new WebhookSignatureError("MISSING_SECRET", "At least one signing secret is required");
3047
+ }
3048
+ let headerValue = pickHeaderValue(readHeader(headers, IQAUTH_SIGNATURE_HEADER));
3049
+ let usedHeader = IQAUTH_SIGNATURE_HEADER;
3050
+ if (!headerValue) {
3051
+ for (const legacy of LEGACY_SIGNATURE_HEADERS) {
3052
+ const v = pickHeaderValue(readHeader(headers, legacy));
3053
+ if (v) {
3054
+ headerValue = v;
3055
+ usedHeader = legacy;
3056
+ const log = opts.onDeprecation ?? ((m) => console.warn(m));
3057
+ log(
3058
+ `[iqauth] deprecation: webhook delivery used legacy header "${legacy}"; migrate sender to "X-IQAuth-Signature" (back-compat removed in next minor).`
3059
+ );
3060
+ break;
3061
+ }
3062
+ }
3063
+ }
3064
+ if (!headerValue) {
3065
+ throw new WebhookSignatureError(
3066
+ "MISSING_HEADER",
3067
+ `Missing webhook signature header. Expected "X-IQAuth-Signature" (or one of: ${LEGACY_SIGNATURE_HEADERS.join(", ")}).`
3068
+ );
3069
+ }
3070
+ const { t, v1 } = parseHeader(headerValue);
3071
+ if (v1.length === 0) {
3072
+ throw new WebhookSignatureError(
3073
+ "MALFORMED_HEADER",
3074
+ `Could not parse "${usedHeader}" header value: ${headerValue}`
3075
+ );
3076
+ }
3077
+ const body = toBuffer(rawBody);
3078
+ let verifiedIdx = -1;
3079
+ for (let i = 0; i < secrets.length; i++) {
3080
+ const secret = secrets[i];
3081
+ if (!secret) continue;
3082
+ const { modern, legacy } = computeSignatures(secret, body, t);
3083
+ const expected = Number.isFinite(t) ? legacy : modern;
3084
+ const ok = expected !== null && v1.some((sig) => timingSafeEqualHex(sig.toLowerCase(), expected));
3085
+ if (ok) {
3086
+ verifiedIdx = i;
3087
+ break;
3088
+ }
3089
+ }
3090
+ if (verifiedIdx === -1) {
3091
+ throw new WebhookSignatureError(
3092
+ "SIGNATURE_MISMATCH",
3093
+ "Webhook signature does not match any provided secret"
3094
+ );
3095
+ }
3096
+ let parsed;
3097
+ try {
3098
+ parsed = JSON.parse(body.toString("utf8"));
3099
+ } catch {
3100
+ throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
3101
+ }
3102
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3103
+ envelopeError("Webhook body must be a JSON object");
3104
+ }
3105
+ const { id, type, subject, time, data, tenantId, specversion } = parsed;
3106
+ if (specversion !== "1.0") {
3107
+ envelopeError(`Envelope \`specversion\` must be "1.0" (got: ${JSON.stringify(specversion)})`);
3108
+ }
3109
+ if (typeof id !== "string" || !id) envelopeError("Envelope missing required string `id`");
3110
+ if (typeof type !== "string" || !type) envelopeError("Envelope missing required string `type`");
3111
+ if (typeof subject !== "string" || !subject) envelopeError("Envelope missing required string `subject`");
3112
+ if (typeof time !== "string" || !time) envelopeError("Envelope missing required string `time`");
3113
+ if (typeof tenantId !== "string" || !tenantId) envelopeError("Envelope missing required string `tenantId`");
3114
+ if (data === void 0 || data === null || typeof data !== "object" || Array.isArray(data)) {
3115
+ envelopeError("Envelope `data` must be an object");
3116
+ }
3117
+ const tolerance = opts.toleranceSeconds ?? 300;
3118
+ const eventMs = Date.parse(time);
3119
+ if (!Number.isFinite(eventMs)) envelopeError(`Envelope \`time\` is not a valid ISO timestamp: ${time}`);
3120
+ const nowMs = opts.nowMs ?? Date.now();
3121
+ if (Math.abs(nowMs - eventMs) > tolerance * 1e3) {
3122
+ throw new WebhookSignatureError(
3123
+ "TIMESTAMP_OUT_OF_TOLERANCE",
3124
+ `Envelope time ${time} is outside the ${tolerance}s tolerance window (now=${new Date(nowMs).toISOString()})`
3125
+ );
3126
+ }
3127
+ return {
3128
+ specversion: "1.0",
3129
+ id,
3130
+ type,
3131
+ subject,
3132
+ time,
3133
+ tenantId,
3134
+ data,
3135
+ idempotencyKey: id,
3136
+ verifiedWithSecretIndex: verifiedIdx
3137
+ };
3138
+ }
2549
3139
 
2550
3140
  // src/server/provisioningBridge.ts
3141
+ var ProvisioningError = class extends Error {
3142
+ constructor(code, message) {
3143
+ super(message);
3144
+ this.name = "ProvisioningError";
3145
+ this.code = code;
3146
+ }
3147
+ };
2551
3148
  function defaultIsUniqueViolation(err) {
2552
3149
  if (!err || typeof err !== "object") return false;
2553
3150
  const e = err;
@@ -2559,6 +3156,16 @@ function defaultIsUniqueViolation(err) {
2559
3156
  function createProvisioningBridge(options) {
2560
3157
  const { storage } = options;
2561
3158
  const isUniqueViolation = options.isUniqueViolation ?? defaultIsUniqueViolation;
3159
+ const allowUnverifiedEmailAdopt = options.allowUnverifiedEmailAdopt === true;
3160
+ const emailVerified = (claims) => claims.email_verified === true;
3161
+ const assertAdoptAllowed = (claims) => {
3162
+ if (!allowUnverifiedEmailAdopt && !emailVerified(claims)) {
3163
+ throw new ProvisioningError(
3164
+ "UNVERIFIED_EMAIL_ADOPT_REFUSED",
3165
+ "Refusing to adopt a pre-existing local account from an unverified email (claims.email_verified !== true). Set allowUnverifiedEmailAdopt:true only if your issuer is trusted to never emit unverified emails for adoption."
3166
+ );
3167
+ }
3168
+ };
2562
3169
  const roleOf = (claims) => {
2563
3170
  try {
2564
3171
  return options.roleMapper?.(claims) ?? null;
@@ -2575,6 +3182,7 @@ function createProvisioningBridge(options) {
2575
3182
  if (claims.email) {
2576
3183
  const byEmail = await storage.findByEmail(claims.email);
2577
3184
  if (byEmail) {
3185
+ assertAdoptAllowed(claims);
2578
3186
  if (storage.adoptByEmail) {
2579
3187
  const adopted = await storage.adoptByEmail(byEmail, claims, roleOf(claims));
2580
3188
  return { user: adopted, claims, created: false, adopted: true };
@@ -2590,7 +3198,10 @@ function createProvisioningBridge(options) {
2590
3198
  if (after) return { user: after, claims, created: false, adopted: false };
2591
3199
  if (claims.email) {
2592
3200
  const byEmail = await storage.findByEmail(claims.email);
2593
- if (byEmail) return { user: byEmail, claims, created: false, adopted: true };
3201
+ if (byEmail) {
3202
+ assertAdoptAllowed(claims);
3203
+ return { user: byEmail, claims, created: false, adopted: true };
3204
+ }
2594
3205
  }
2595
3206
  throw err;
2596
3207
  }
@@ -2611,10 +3222,13 @@ function createProvisioningBridge(options) {
2611
3222
  ErrorCodes,
2612
3223
  GdprModule,
2613
3224
  HierarchyModule,
3225
+ IQAUTH_SIGNATURE_HEADER,
2614
3226
  IQAuthClient,
2615
3227
  IQAuthError,
3228
+ IQ_AUTH_ERROR_CODES,
2616
3229
  InMemoryOidcStateStore,
2617
3230
  InvitesModule,
3231
+ LEGACY_SIGNATURE_HEADERS,
2618
3232
  MembershipsModule,
2619
3233
  MfaModule,
2620
3234
  OidcModule,
@@ -2632,14 +3246,19 @@ function createProvisioningBridge(options) {
2632
3246
  WebhookSignatureError,
2633
3247
  WebhooksModule,
2634
3248
  assertPublishableKey,
3249
+ buildUserinfoResponse,
2635
3250
  createProvisioningBridge,
2636
3251
  createTestIssuer,
2637
3252
  encodePublishableKey,
3253
+ expandPermissions,
3254
+ handleUserinfo,
3255
+ hasPermission,
2638
3256
  iqAuthMiddleware,
2639
3257
  isPublishableKey,
2640
3258
  isSecretKey,
2641
3259
  isValidWebhookSignature,
2642
3260
  parsePublishableKey,
3261
+ parseWebhookEvent,
2643
3262
  verifyWebhookSignature,
2644
3263
  verifyWsUpgrade
2645
3264
  });