@iqauth/sdk 2.6.3 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +173 -1
  2. package/dist/browser-session.d.mts +4 -4
  3. package/dist/browser-session.d.ts +4 -4
  4. package/dist/browser-session.js +181 -41
  5. package/dist/browser-session.mjs +3 -3
  6. package/dist/browser.d.mts +5 -5
  7. package/dist/browser.d.ts +5 -5
  8. package/dist/browser.js +271 -32
  9. package/dist/browser.mjs +10 -8
  10. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  11. package/dist/chunk-C2ZTBOAC.mjs +36 -0
  12. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  13. package/dist/chunk-GLXSIGVS.mjs +66 -0
  14. package/dist/{chunk-TKZTCPEK.mjs → chunk-GN37E64I.mjs} +32 -40
  15. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  16. package/dist/{chunk-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
  17. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  18. package/dist/chunk-PMAFENVI.mjs +229 -0
  19. package/dist/chunk-RR2MGPTK.mjs +2724 -0
  20. package/dist/{chunk-76W5TLQQ.mjs → chunk-RTJAIBXY.mjs} +220 -20
  21. package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
  22. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  23. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  24. package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
  25. package/dist/cli/index.js +2 -2
  26. package/dist/cli/index.mjs +2 -2
  27. package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
  28. package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
  29. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  30. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  31. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  32. package/dist/{express-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
  33. package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
  34. package/dist/express.d.mts +7 -6
  35. package/dist/express.d.ts +7 -6
  36. package/dist/express.js +349 -52
  37. package/dist/express.mjs +39 -12
  38. package/dist/fastify.d.mts +2 -0
  39. package/dist/fastify.d.ts +2 -0
  40. package/dist/fastify.js +332 -52
  41. package/dist/fastify.mjs +23 -8
  42. package/dist/hono.d.mts +2 -0
  43. package/dist/hono.d.ts +2 -0
  44. package/dist/hono.js +329 -52
  45. package/dist/hono.mjs +20 -8
  46. package/dist/index-5KSZEnDe.d.ts +1626 -0
  47. package/dist/index-CKoZHAoc.d.mts +1626 -0
  48. package/dist/index.d.mts +56 -8
  49. package/dist/index.d.ts +56 -8
  50. package/dist/index.js +565 -69
  51. package/dist/index.mjs +29 -9
  52. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  53. package/dist/locales.d.mts +1 -1
  54. package/dist/locales.d.ts +1 -1
  55. package/dist/mobile.d.mts +77 -7
  56. package/dist/mobile.d.ts +77 -7
  57. package/dist/mobile.js +276 -41
  58. package/dist/mobile.mjs +98 -3
  59. package/dist/next.d.mts +2 -1
  60. package/dist/next.d.ts +2 -1
  61. package/dist/next.js +391 -201
  62. package/dist/next.mjs +22 -7
  63. package/dist/pkce-7WKV4OIN.mjs +11 -0
  64. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
  65. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
  66. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  67. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  68. package/dist/react-permissions.d.mts +52 -0
  69. package/dist/react-permissions.d.ts +52 -0
  70. package/dist/react-permissions.js +239 -0
  71. package/dist/react-permissions.mjs +97 -0
  72. package/dist/react.d.mts +9 -1624
  73. package/dist/react.d.ts +9 -1624
  74. package/dist/react.js +343 -36
  75. package/dist/react.mjs +59 -2611
  76. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  77. package/dist/server/handlers.d.mts +148 -3
  78. package/dist/server/handlers.d.ts +148 -3
  79. package/dist/server/handlers.js +410 -11
  80. package/dist/server/handlers.mjs +12 -3
  81. package/dist/server.d.mts +151 -8
  82. package/dist/server.d.ts +151 -8
  83. package/dist/server.js +406 -50
  84. package/dist/server.mjs +93 -11
  85. package/dist/service.d.mts +4 -4
  86. package/dist/service.d.ts +4 -4
  87. package/dist/service.js +181 -41
  88. package/dist/service.mjs +3 -3
  89. package/dist/{signIn-CiIBTJIh.d.mts → signIn-BLFnz8SV.d.ts} +78 -3
  90. package/dist/{signIn-CCY4JE5G.mjs → signIn-SHBW6Z4T.mjs} +2 -1
  91. package/dist/{signIn-OCr88Zf8.d.ts → signIn-T-CZ6t6r.d.mts} +78 -3
  92. package/dist/test.mjs +3 -3
  93. package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
  94. package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
  95. package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
  96. package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
  97. package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
  98. package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
  99. package/dist/webhooks.d.mts +100 -17
  100. package/dist/webhooks.d.ts +100 -17
  101. package/dist/webhooks.js +164 -15
  102. package/dist/webhooks.mjs +7 -1
  103. package/dist/ws.d.mts +2 -2
  104. package/dist/ws.d.ts +2 -2
  105. package/dist/ws.js +80 -30
  106. package/dist/ws.mjs +4 -4
  107. package/docs/error-handling.md +101 -0
  108. package/docs/guides/effective-permissions.md +171 -0
  109. package/package.json +13 -3
  110. package/dist/chunk-UKZLOHZG.mjs +0 -83
  111. package/dist/errors-CDdl24MP.d.mts +0 -52
  112. package/dist/errors-CDdl24MP.d.ts +0 -52
@@ -1,8 +1,8 @@
1
- import { S as SessionManager } from './signIn-CiIBTJIh.mjs';
2
- export { n as AccountRecord, A as AccountRegistry, C as CallbackResult, k as LinkProviderInput, L as LinkedIdentity, M as MagicLinkRequestInput, m as MultiAccountTokenStore, j as PasskeyAuthInput, P as PasswordlessOptions, z as REFRESH_COOKIE, R as RefreshTokenStore, a as SessionManagerOptions, b as SessionSnapshot, c as SessionStatus, x as SignInOptions, y as SignOutOptions, U as UnlinkProviderInput, d as beginPasskeyAuthentication, e as beginPasskeyRegistration, o as buildSignInUrl, B as clearCookie, h as enrollPasskey, f as finishPasskeyAuthentication, g as finishPasskeyRegistration, D as getCookie, p as handleAuthCallback, i as linkProvider, l as listLinkedIdentities, q as redirectToSignIn, r as requestMagicLink, E as setCookie, t as signIn, s as signInWithPasskey, w as signOut, u as unlinkProvider, v as verifyMagicLink } from './signIn-CiIBTJIh.mjs';
3
- export { K as KeyMode, c as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, b as isSecretKey, p as parsePublishableKey } from './publishableKey-BaR0HoAH.mjs';
4
- export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
5
- import './types-DZAflmmq.mjs';
1
+ import { S as SessionManager } from './signIn-T-CZ6t6r.mjs';
2
+ export { p as AccountRecord, A as AccountRegistry, C as CallbackResult, d as LinkProviderInput, L as LinkedIdentity, M as MagicLinkRequestInput, o as MultiAccountTokenStore, n as PasskeyAuthInput, P as PasswordlessOptions, z as REFRESH_COOKIE, R as RefreshTokenStore, e as SessionManagerOptions, a as SessionSnapshot, f as SessionStatus, b as SignInOptions, c as SignOutOptions, U as UnlinkProviderInput, g as beginPasskeyAuthentication, i as beginPasskeyRegistration, q as buildSignInUrl, B as clearCookie, k as enrollPasskey, h as finishPasskeyAuthentication, j as finishPasskeyRegistration, D as getCookie, t as handleAuthCallback, m as linkProvider, l as listLinkedIdentities, w as redirectToSignIn, r as requestMagicLink, E as setCookie, x as signIn, s as signInWithPasskey, y as signOut, u as unlinkProvider, v as verifyMagicLink } from './signIn-T-CZ6t6r.mjs';
3
+ export { K as KeyMode, c as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, b as isSecretKey, p as parsePublishableKey } from './publishableKey-f2kq-rKw.mjs';
4
+ export { b as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.mjs';
5
+ import './types-XOV9XPVi.mjs';
6
6
 
7
7
  /**
8
8
  * Browser-safe PKCE + state/nonce generation using WebCrypto.
package/dist/browser.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { S as SessionManager } from './signIn-OCr88Zf8.js';
2
- export { n as AccountRecord, A as AccountRegistry, C as CallbackResult, k as LinkProviderInput, L as LinkedIdentity, M as MagicLinkRequestInput, m as MultiAccountTokenStore, j as PasskeyAuthInput, P as PasswordlessOptions, z as REFRESH_COOKIE, R as RefreshTokenStore, a as SessionManagerOptions, b as SessionSnapshot, c as SessionStatus, x as SignInOptions, y as SignOutOptions, U as UnlinkProviderInput, d as beginPasskeyAuthentication, e as beginPasskeyRegistration, o as buildSignInUrl, B as clearCookie, h as enrollPasskey, f as finishPasskeyAuthentication, g as finishPasskeyRegistration, D as getCookie, p as handleAuthCallback, i as linkProvider, l as listLinkedIdentities, q as redirectToSignIn, r as requestMagicLink, E as setCookie, t as signIn, s as signInWithPasskey, w as signOut, u as unlinkProvider, v as verifyMagicLink } from './signIn-OCr88Zf8.js';
3
- export { K as KeyMode, c as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, b as isSecretKey, p as parsePublishableKey } from './publishableKey-BaR0HoAH.js';
4
- export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
5
- import './types-DZAflmmq.js';
1
+ import { S as SessionManager } from './signIn-BLFnz8SV.js';
2
+ export { p as AccountRecord, A as AccountRegistry, C as CallbackResult, d as LinkProviderInput, L as LinkedIdentity, M as MagicLinkRequestInput, o as MultiAccountTokenStore, n as PasskeyAuthInput, P as PasswordlessOptions, z as REFRESH_COOKIE, R as RefreshTokenStore, e as SessionManagerOptions, a as SessionSnapshot, f as SessionStatus, b as SignInOptions, c as SignOutOptions, U as UnlinkProviderInput, g as beginPasskeyAuthentication, i as beginPasskeyRegistration, q as buildSignInUrl, B as clearCookie, k as enrollPasskey, h as finishPasskeyAuthentication, j as finishPasskeyRegistration, D as getCookie, t as handleAuthCallback, m as linkProvider, l as listLinkedIdentities, w as redirectToSignIn, r as requestMagicLink, E as setCookie, x as signIn, s as signInWithPasskey, y as signOut, u as unlinkProvider, v as verifyMagicLink } from './signIn-BLFnz8SV.js';
3
+ export { K as KeyMode, c as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, b as isSecretKey, p as parsePublishableKey } from './publishableKey-f2kq-rKw.js';
4
+ export { b as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.js';
5
+ import './types-XOV9XPVi.js';
6
6
 
7
7
  /**
8
8
  * Browser-safe PKCE + state/nonce generation using WebCrypto.
package/dist/browser.js CHANGED
@@ -444,13 +444,30 @@ __export(browser_exports, {
444
444
  module.exports = __toCommonJS(browser_exports);
445
445
 
446
446
  // src/errors.ts
447
- var IQAuthError = class extends Error {
448
- constructor(code, message, status, raw) {
447
+ var IQAuthError = class _IQAuthError extends Error {
448
+ constructor(code, message, status, cause) {
449
449
  super(message);
450
450
  this.name = "IQAuthError";
451
451
  this.code = code;
452
452
  this.status = status;
453
- this.raw = raw;
453
+ this.cause = cause;
454
+ this.raw = cause;
455
+ }
456
+ /**
457
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
458
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
459
+ */
460
+ static isIQAuthError(value) {
461
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
462
+ }
463
+ /**
464
+ * Type-narrowed code check. Lets callers write
465
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
466
+ * taxonomy without losing the ability to handle server codes via
467
+ * `err.code === "TOKEN_REVOKED"`.
468
+ */
469
+ is(code) {
470
+ return this.code === code;
454
471
  }
455
472
  };
456
473
  var ErrorCodes = {
@@ -549,14 +566,14 @@ function assertPublishableKey(raw, opts) {
549
566
  const ctx = opts?.context ? `${opts.context}: ` : "";
550
567
  if (typeof raw !== "string" || raw.length === 0) {
551
568
  throw new IQAuthError(
552
- "CONFIG_INVALID",
569
+ "config_invalid",
553
570
  `${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.`
554
571
  );
555
572
  }
556
573
  const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
557
574
  if (!shapeMatch) {
558
575
  throw new IQAuthError(
559
- "CONFIG_INVALID",
576
+ "config_invalid",
560
577
  `${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.`
561
578
  );
562
579
  }
@@ -565,19 +582,19 @@ function assertPublishableKey(raw, opts) {
565
582
  decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
566
583
  } catch {
567
584
  throw new IQAuthError(
568
- "CONFIG_INVALID",
585
+ "config_invalid",
569
586
  `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
570
587
  );
571
588
  }
572
589
  if (!isPublishableKeyPayload(decoded)) {
573
590
  throw new IQAuthError(
574
- "CONFIG_INVALID",
591
+ "config_invalid",
575
592
  `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
576
593
  );
577
594
  }
578
595
  if (!isValidIssuerUrl(decoded.iss)) {
579
596
  throw new IQAuthError(
580
- "CONFIG_INVALID",
597
+ "config_invalid",
581
598
  `${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.`
582
599
  );
583
600
  }
@@ -656,6 +673,7 @@ function clearPkce(state) {
656
673
  }
657
674
 
658
675
  // src/browser/sessionManager.ts
676
+ var PROBE_WAIT_MS = 80;
659
677
  var DEFAULT_REFRESH_PATH = "/api/v1/auth/refresh";
660
678
  var DEFAULT_USERINFO_PATH = "/api/v1/auth/me";
661
679
  async function readAuthErrorCode(res) {
@@ -693,7 +711,13 @@ function claimsToSessionUser(claims) {
693
711
  tenantId: claims.tenantId,
694
712
  vendorId: claims.vendorId,
695
713
  roles: claims.roles ?? [],
696
- entitlements: claims.entitlements ?? []
714
+ entitlements: claims.entitlements ?? [],
715
+ // SDK 2.7.0 (Task #124) — pass through identity claims when issued.
716
+ ...claims.picture !== void 0 ? { picture: claims.picture } : {},
717
+ ...claims.email_verified !== void 0 ? { emailVerified: claims.email_verified } : {},
718
+ ...claims.given_name !== void 0 ? { givenName: claims.given_name } : {},
719
+ ...claims.family_name !== void 0 ? { familyName: claims.family_name } : {},
720
+ ...claims.locale !== void 0 ? { locale: claims.locale } : {}
697
721
  };
698
722
  }
699
723
  var EMPTY = {
@@ -717,11 +741,50 @@ var NO_OP_STORE = {
717
741
  write: () => void 0,
718
742
  clear: () => void 0
719
743
  };
744
+ var IDEMPOTENCY_HEADER = "X-IQAuth-Idempotency";
745
+ function randomIdempotencyToken() {
746
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
747
+ const bytes = new Uint8Array(16);
748
+ crypto.getRandomValues(bytes);
749
+ let out = "";
750
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
751
+ return out;
752
+ }
753
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
754
+ }
720
755
  var SessionManager = class {
721
756
  constructor(options) {
722
757
  this.snapshot = { ...EMPTY };
723
758
  this.listeners = /* @__PURE__ */ new Set();
724
759
  this.refreshPromise = null;
760
+ /**
761
+ * Cancellation handle for the in-flight refresh, if any. `signOut()` (or a
762
+ * `session:signout` broadcast from another tab) calls `abort()` so the
763
+ * refresh response is dropped before it can write a fresh access cookie
764
+ * on top of the just-cleared session — the second root cause of "ghost
765
+ * signed-in" sessions after Sign Out.
766
+ */
767
+ this.refreshAbort = null;
768
+ /**
769
+ * Set to `true` by `signOut()` / `signOutLocal()` for the lifetime of the
770
+ * call. Used as a safety belt: even if a refresh response arrives while
771
+ * `refreshAbort` was unable to interrupt the network call (e.g. the body
772
+ * was already streaming back), `runRefresh` checks this flag before
773
+ * mutating session state and bails out.
774
+ */
775
+ this.signoutInProgress = false;
776
+ /**
777
+ * Per-session opaque idempotency token. Sent as `X-IQAuth-Idempotency` on
778
+ * every /refresh and /signout request the SDK makes through a framework
779
+ * adapter (Express/Fastify/Hono/Next), so the adapter's `SignoutRegistry`
780
+ * can collapse a refresh that lands moments after a signout — even when
781
+ * the two requests are routed to different server instances (multi-replica
782
+ * deployments).
783
+ *
784
+ * Generated lazily on first use, rotated on signout so the next session
785
+ * starts with a fresh token. Opaque random — never the raw refresh token.
786
+ */
787
+ this.idempotencyToken = null;
725
788
  this.channel = null;
726
789
  this.proactiveTimer = null;
727
790
  this.bootstrapped = false;
@@ -729,6 +792,8 @@ var SessionManager = class {
729
792
  this.remoteRefreshWaiters = [];
730
793
  /** Active claims by other tabs (keyed by source tabId). */
731
794
  this.foreignClaim = null;
795
+ /** Resolver for an in-flight cross-tab `session:probe`, set during bootstrap. */
796
+ this.probeResolver = null;
732
797
  const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
733
798
  this.key = parsed;
734
799
  const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
@@ -741,6 +806,8 @@ var SessionManager = class {
741
806
  this.refreshCookieName = options.cookieNames?.refresh ?? REFRESH_COOKIE;
742
807
  this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore(this.refreshCookieName) : NO_OP_STORE);
743
808
  this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
809
+ this.debug = options.debug ?? false;
810
+ this.onTimingEvent = options.onTimingEvent ?? null;
744
811
  this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
745
812
  throw new Error("global fetch is not available; pass fetchImpl");
746
813
  }));
@@ -767,10 +834,35 @@ var SessionManager = class {
767
834
  get issuerUrl() {
768
835
  return this.issuer;
769
836
  }
837
+ /**
838
+ * SDK 2.7.0 (Task #124) — The hosted IQAuth host derived from the
839
+ * publishable key's `iss` claim, normalized to URL form. This is what
840
+ * `<SignIn/>` and `buildSignInUrl` use to talk to the hosted UI; it
841
+ * deliberately ignores the `issuer` constructor override so a misrouted
842
+ * `issuer` (e.g. pointed at the consumer app's own domain) cannot break
843
+ * the hosted flow. Use {@link issuerUrl} for token / discovery endpoints.
844
+ */
845
+ get hostedIssuerUrl() {
846
+ const iss = this.key.iss;
847
+ return (iss.startsWith("http") ? iss : `https://${iss}`).replace(/\/+$/, "");
848
+ }
770
849
  /** Cookie name the SDK uses for the refresh token (overridable via `cookieNames.refresh`). */
771
850
  get refreshCookie() {
772
851
  return this.refreshCookieName;
773
852
  }
853
+ /**
854
+ * Returns the current per-session idempotency token, generating one
855
+ * lazily on first use. Sent as the `X-IQAuth-Idempotency` header on
856
+ * /refresh and /signout requests so the framework adapter's
857
+ * `SignoutRegistry` can collapse a refresh-vs-signout race even across
858
+ * server instances.
859
+ */
860
+ getIdempotencyToken() {
861
+ if (!this.idempotencyToken) {
862
+ this.idempotencyToken = randomIdempotencyToken();
863
+ }
864
+ return this.idempotencyToken;
865
+ }
774
866
  getSnapshot() {
775
867
  return this.snapshot;
776
868
  }
@@ -784,9 +876,44 @@ var SessionManager = class {
784
876
  * One-time bootstrap: warm the session from the refresh cookie if present.
785
877
  * Safe to call multiple times.
786
878
  */
879
+ /**
880
+ * Task #126: Public timing-event emitter. Used by the browser sign-in
881
+ * helpers (redirectToSignIn / handleAuthCallback) to surface signIn-phase
882
+ * timings through the same `debug` + `onTimingEvent` channel as
883
+ * bootstrap/refresh. Safe to call from anywhere — internal callers
884
+ * pre-compute durationMs.
885
+ */
886
+ recordTiming(phase, durationMs, ok, code) {
887
+ this.emitTiming(phase, durationMs, ok, code);
888
+ }
889
+ /** Task #126: emit a session timing event to debug log + onTimingEvent hook. */
890
+ emitTiming(phase, durationMs, ok, code) {
891
+ if (this.debug) {
892
+ try {
893
+ console.debug("[iqauth_session]", { phase, durationMs, ok, code });
894
+ } catch {
895
+ }
896
+ }
897
+ if (this.onTimingEvent) {
898
+ try {
899
+ this.onTimingEvent({ phase, durationMs, ok, code });
900
+ } catch {
901
+ }
902
+ }
903
+ }
787
904
  async bootstrap() {
788
905
  if (this.bootstrapped) return;
789
906
  this.bootstrapped = true;
907
+ const t0 = Date.now();
908
+ try {
909
+ await this.bootstrapInner();
910
+ this.emitTiming("bootstrap", Date.now() - t0, this.snapshot.status === "authenticated");
911
+ } catch (err) {
912
+ this.emitTiming("bootstrap", Date.now() - t0, false, err instanceof Error ? err.message : "ERROR");
913
+ throw err;
914
+ }
915
+ }
916
+ async bootstrapInner() {
790
917
  if (this.serverManagedSession) {
791
918
  try {
792
919
  const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
@@ -821,6 +948,15 @@ var SessionManager = class {
821
948
  return;
822
949
  }
823
950
  }
951
+ const peerSnapshot = await this.probePeers();
952
+ if (peerSnapshot && peerSnapshot.status === "authenticated") {
953
+ this.update({
954
+ ...peerSnapshot,
955
+ version: Math.max(this.snapshot.version, peerSnapshot.version) + 1
956
+ });
957
+ this.scheduleProactiveRefresh();
958
+ return;
959
+ }
824
960
  const stored = await Promise.resolve(this.tokenStore.read());
825
961
  if (!stored) {
826
962
  this.setStatus("unauthenticated");
@@ -829,6 +965,22 @@ var SessionManager = class {
829
965
  const ok = await this.refresh();
830
966
  if (!ok) this.setStatus("unauthenticated");
831
967
  }
968
+ probePeers() {
969
+ if (!this.channel) return Promise.resolve(null);
970
+ return new Promise((resolve) => {
971
+ let settled = false;
972
+ const finish = (snap) => {
973
+ if (settled) return;
974
+ settled = true;
975
+ this.probeResolver = null;
976
+ clearTimeout(timer);
977
+ resolve(snap);
978
+ };
979
+ this.probeResolver = (snap) => finish(snap);
980
+ const timer = setTimeout(() => finish(null), PROBE_WAIT_MS);
981
+ this.broadcastEnvelope({ type: "session:probe", source: this.tabId, ts: Date.now() });
982
+ });
983
+ }
832
984
  /**
833
985
  * Single-flight token refresh, coordinated across tabs via BroadcastChannel.
834
986
  *
@@ -842,30 +994,48 @@ var SessionManager = class {
842
994
  */
843
995
  refresh() {
844
996
  if (this.refreshPromise) return this.refreshPromise;
845
- this.refreshPromise = this.runRefresh().finally(() => {
997
+ const t0 = Date.now();
998
+ this.refreshPromise = this.runRefresh().then((ok) => {
999
+ this.emitTiming("refresh", Date.now() - t0, ok, ok ? void 0 : this.snapshot.error?.code ?? "REFRESH_FAILED");
1000
+ return ok;
1001
+ }).finally(() => {
846
1002
  this.refreshPromise = null;
847
1003
  });
848
1004
  return this.refreshPromise;
849
1005
  }
850
1006
  async runRefresh() {
851
- const myClaim = { source: this.tabId, ts: Date.now() };
852
- if (this.channel) {
853
- this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
854
- await new Promise((r) => setTimeout(r, 25));
855
- const foreign = this.foreignClaim;
856
- if (foreign && this.claimWins(foreign, myClaim)) {
857
- return this.waitForForeignRefresh();
858
- }
859
- }
1007
+ const abort = new AbortController();
1008
+ this.refreshAbort = abort;
860
1009
  try {
1010
+ const myClaim = { source: this.tabId, ts: Date.now() };
1011
+ if (this.channel) {
1012
+ this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
1013
+ await new Promise((r) => setTimeout(r, 25));
1014
+ if (abort.signal.aborted || this.signoutInProgress) {
1015
+ this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
1016
+ return false;
1017
+ }
1018
+ const foreign = this.foreignClaim;
1019
+ if (foreign && this.claimWins(foreign, myClaim)) {
1020
+ return this.waitForForeignRefresh();
1021
+ }
1022
+ }
861
1023
  const refreshToken = await Promise.resolve(this.tokenStore.read());
862
1024
  const res = await this.fetchImpl(`${this.issuer}${this.refreshPath}`, {
863
1025
  method: "POST",
864
1026
  credentials: "include",
865
- headers: { "Content-Type": "application/json" },
866
- body: JSON.stringify(refreshToken ? { refreshToken } : {})
1027
+ headers: {
1028
+ "Content-Type": "application/json",
1029
+ [IDEMPOTENCY_HEADER]: this.getIdempotencyToken()
1030
+ },
1031
+ body: JSON.stringify(refreshToken ? { refreshToken } : {}),
1032
+ signal: abort.signal
867
1033
  });
868
1034
  const body = await res.json().catch(() => ({}));
1035
+ if (this.signoutInProgress || abort.signal.aborted) {
1036
+ this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
1037
+ return false;
1038
+ }
869
1039
  const data = body.data;
870
1040
  if (!res.ok || !body.success || !data?.accessToken) {
871
1041
  const err = body.error;
@@ -884,14 +1054,18 @@ var SessionManager = class {
884
1054
  this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: true });
885
1055
  return true;
886
1056
  } catch (err) {
887
- this.setError({
888
- code: "NETWORK_ERROR",
889
- message: err instanceof Error ? err.message : "Refresh request failed"
890
- });
1057
+ const aborted = err?.name === "AbortError" || abort.signal.aborted || this.signoutInProgress;
1058
+ if (!aborted) {
1059
+ this.setError({
1060
+ code: "NETWORK_ERROR",
1061
+ message: err instanceof Error ? err.message : "Refresh request failed"
1062
+ });
1063
+ }
891
1064
  this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
892
1065
  return false;
893
1066
  } finally {
894
1067
  this.foreignClaim = null;
1068
+ if (this.refreshAbort === abort) this.refreshAbort = null;
895
1069
  }
896
1070
  }
897
1071
  claimWins(foreign, mine) {
@@ -999,6 +1173,14 @@ var SessionManager = class {
999
1173
  * the server-side logout request.
1000
1174
  */
1001
1175
  signOutLocal(status = "unauthenticated") {
1176
+ this.signoutInProgress = true;
1177
+ if (this.refreshAbort) {
1178
+ try {
1179
+ this.refreshAbort.abort();
1180
+ } catch {
1181
+ }
1182
+ this.refreshAbort = null;
1183
+ }
1002
1184
  void Promise.resolve(this.tokenStore.clear());
1003
1185
  if (this.proactiveTimer) {
1004
1186
  clearTimeout(this.proactiveTimer);
@@ -1013,7 +1195,12 @@ var SessionManager = class {
1013
1195
  error: null,
1014
1196
  version: this.snapshot.version + 1
1015
1197
  });
1198
+ this.broadcastEnvelope({ type: "refresh:abort", source: this.tabId, ts: Date.now() });
1016
1199
  this.broadcast("session:signout");
1200
+ this.idempotencyToken = null;
1201
+ setTimeout(() => {
1202
+ this.signoutInProgress = false;
1203
+ }, 0);
1017
1204
  }
1018
1205
  /**
1019
1206
  * Replace the refresh-token store at runtime. Used by the F22
@@ -1077,6 +1264,12 @@ var SessionManager = class {
1077
1264
  }
1078
1265
  onBroadcast(env) {
1079
1266
  if (!env || env.source === this.tabId) return;
1267
+ if (env.type === "session:probe") {
1268
+ if (this.snapshot.status === "authenticated") {
1269
+ this.broadcast("session:update");
1270
+ }
1271
+ return;
1272
+ }
1080
1273
  if (env.type === "refresh:claim") {
1081
1274
  this.foreignClaim = { source: env.source, ts: env.ts };
1082
1275
  return;
@@ -1089,6 +1282,24 @@ var SessionManager = class {
1089
1282
  this.foreignClaim = null;
1090
1283
  return;
1091
1284
  }
1285
+ if (env.type === "refresh:abort") {
1286
+ this.signoutInProgress = true;
1287
+ if (this.refreshAbort) {
1288
+ try {
1289
+ this.refreshAbort.abort();
1290
+ } catch {
1291
+ }
1292
+ this.refreshAbort = null;
1293
+ }
1294
+ const waiters = this.remoteRefreshWaiters;
1295
+ this.remoteRefreshWaiters = [];
1296
+ for (const w of waiters) w(false);
1297
+ this.foreignClaim = null;
1298
+ setTimeout(() => {
1299
+ this.signoutInProgress = false;
1300
+ }, 0);
1301
+ return;
1302
+ }
1092
1303
  if (env.type === "session:signout") {
1093
1304
  this.update({
1094
1305
  status: "unauthenticated",
@@ -1102,6 +1313,12 @@ var SessionManager = class {
1102
1313
  return;
1103
1314
  }
1104
1315
  if ((env.type === "session:update" || env.type === "session:refresh") && env.payload) {
1316
+ if (this.probeResolver && env.payload.status === "authenticated") {
1317
+ const r = this.probeResolver;
1318
+ this.probeResolver = null;
1319
+ r(env.payload);
1320
+ return;
1321
+ }
1105
1322
  this.update({
1106
1323
  ...env.payload,
1107
1324
  version: Math.max(this.snapshot.version, env.payload.version) + 1
@@ -1386,7 +1603,7 @@ async function buildSignInUrl(manager, opts = {}) {
1386
1603
  returnTo,
1387
1604
  createdAt: Date.now()
1388
1605
  });
1389
- const url2 = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.issuerUrl);
1606
+ const url2 = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.hostedIssuerUrl);
1390
1607
  url2.searchParams.set("response_type", "code");
1391
1608
  url2.searchParams.set("app", manager.appKey);
1392
1609
  url2.searchParams.set("publishable_key", manager.publishableKey.raw);
@@ -1401,33 +1618,50 @@ async function buildSignInUrl(manager, opts = {}) {
1401
1618
  return url2.toString();
1402
1619
  }
1403
1620
  async function redirectToSignIn(manager, opts = {}) {
1404
- const url2 = await buildSignInUrl(manager, opts);
1405
- if (typeof window === "undefined") {
1406
- throw new Error("redirectToSignIn requires a browser environment");
1621
+ const t0 = Date.now();
1622
+ let ok = false;
1623
+ let code;
1624
+ try {
1625
+ const url2 = await buildSignInUrl(manager, opts);
1626
+ if (typeof window === "undefined") {
1627
+ code = "NO_WINDOW";
1628
+ throw new Error("redirectToSignIn requires a browser environment");
1629
+ }
1630
+ ok = true;
1631
+ manager.recordTiming("signIn", Date.now() - t0, true);
1632
+ window.location.assign(url2);
1633
+ } catch (err) {
1634
+ if (!ok) manager.recordTiming("signIn", Date.now() - t0, false, code ?? (err instanceof Error ? err.message : "ERROR"));
1635
+ throw err;
1407
1636
  }
1408
- window.location.assign(url2);
1409
1637
  }
1410
1638
  async function signIn(manager, opts = {}) {
1411
1639
  return redirectToSignIn(manager, opts);
1412
1640
  }
1413
1641
  async function handleAuthCallback(manager, options = {}) {
1642
+ const t0 = Date.now();
1643
+ const emit = (ok, code2) => manager.recordTiming("signIn", Date.now() - t0, ok, code2);
1414
1644
  const url2 = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
1415
1645
  const code = url2.searchParams.get("code");
1416
1646
  const state = url2.searchParams.get("state");
1417
1647
  const errorParam = url2.searchParams.get("error");
1418
1648
  if (errorParam) {
1649
+ emit(false, errorParam);
1419
1650
  return { ok: false, returnTo: "/", error: errorParam };
1420
1651
  }
1421
1652
  if (!code || !state) {
1653
+ emit(false, "missing_code_or_state");
1422
1654
  return { ok: false, returnTo: "/", error: "missing_code_or_state" };
1423
1655
  }
1424
1656
  const record = loadPkce(state);
1425
1657
  if (!record) {
1658
+ emit(false, "unknown_state");
1426
1659
  return { ok: false, returnTo: "/", error: "unknown_state" };
1427
1660
  }
1428
1661
  clearPkce(state);
1429
1662
  const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
1430
1663
  if (!fetchImpl) {
1664
+ emit(false, "no_fetch");
1431
1665
  return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
1432
1666
  }
1433
1667
  const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
@@ -1446,10 +1680,12 @@ async function handleAuthCallback(manager, options = {}) {
1446
1680
  const body = await res.json().catch(() => ({}));
1447
1681
  if (!res.ok) {
1448
1682
  const desc = body.error_description ?? body.error ?? "token_exchange_failed";
1683
+ emit(false, desc);
1449
1684
  return { ok: false, returnTo: record.returnTo, error: desc };
1450
1685
  }
1451
1686
  const tokens = body;
1452
1687
  if (!tokens.access_token) {
1688
+ emit(false, "missing_access_token");
1453
1689
  return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
1454
1690
  }
1455
1691
  if (tokens.refresh_token) {
@@ -1457,21 +1693,24 @@ async function handleAuthCallback(manager, options = {}) {
1457
1693
  setCookie(cookieName, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
1458
1694
  }
1459
1695
  manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
1696
+ emit(true);
1460
1697
  return { ok: true, returnTo: record.returnTo };
1461
1698
  }
1462
1699
  async function signOut(manager, opts = {}) {
1463
1700
  if (!opts.localOnly) {
1464
1701
  const issuer = manager.issuerUrl.replace(/\/$/, "");
1702
+ const idempotency = manager.getIdempotencyToken();
1465
1703
  try {
1466
1704
  const url2 = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
1467
- await manager.fetch(url2, { method: "POST" }).catch(() => void 0);
1705
+ await manager.fetch(url2, { method: "POST", headers: { "X-IQAuth-Idempotency": idempotency } }).catch(() => void 0);
1468
1706
  } catch {
1469
1707
  }
1470
1708
  if (opts.endSsoSession !== false) {
1471
1709
  try {
1472
1710
  await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
1473
1711
  method: "POST",
1474
- credentials: "include"
1712
+ credentials: "include",
1713
+ headers: { "X-IQAuth-Idempotency": idempotency }
1475
1714
  }).catch(() => void 0);
1476
1715
  } catch {
1477
1716
  }
package/dist/browser.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  exitImpersonation,
5
5
  reverify,
6
6
  withReverification
7
- } from "./chunk-LIZYFXH7.mjs";
7
+ } from "./chunk-DFWHSDYQ.mjs";
8
8
  import {
9
9
  AccountRegistry,
10
10
  MultiAccountTokenStore,
@@ -20,31 +20,33 @@ import {
20
20
  signInWithPasskey,
21
21
  unlinkProvider,
22
22
  verifyMagicLink
23
- } from "./chunk-76W5TLQQ.mjs";
23
+ } from "./chunk-RTJAIBXY.mjs";
24
24
  import {
25
25
  REFRESH_COOKIE,
26
26
  buildSignInUrl,
27
27
  clearCookie,
28
- createPkcePair,
29
28
  getCookie,
30
29
  handleAuthCallback,
31
- randomUrlSafe,
32
30
  redirectToSignIn,
33
- s256Challenge,
34
31
  setCookie,
35
32
  signIn,
36
33
  signOut
37
- } from "./chunk-TKZTCPEK.mjs";
34
+ } from "./chunk-GN37E64I.mjs";
35
+ import {
36
+ createPkcePair,
37
+ randomUrlSafe,
38
+ s256Challenge
39
+ } from "./chunk-C2ZTBOAC.mjs";
38
40
  import {
39
41
  encodePublishableKey,
40
42
  isPublishableKey,
41
43
  isSecretKey,
42
44
  parsePublishableKey
43
- } from "./chunk-WQWBJSSS.mjs";
45
+ } from "./chunk-HVHNYPDC.mjs";
44
46
  import {
45
47
  ErrorCodes,
46
48
  IQAuthError
47
- } from "./chunk-6I6RM4MN.mjs";
49
+ } from "./chunk-6PJRLRB4.mjs";
48
50
  import "./chunk-Y6FXYEAI.mjs";
49
51
  export {
50
52
  AccountRegistry,
@@ -1,11 +1,40 @@
1
1
  // src/errors.ts
2
- var IQAuthError = class extends Error {
3
- constructor(code, message, status, raw) {
2
+ var IQ_AUTH_ERROR_CODES = [
3
+ "token_expired",
4
+ "token_invalid",
5
+ "jwks_unavailable",
6
+ "jwks_fetch_failed",
7
+ "rate_limited",
8
+ "network",
9
+ "config_invalid",
10
+ "app_not_found",
11
+ "permission_denied",
12
+ "unknown"
13
+ ];
14
+ var IQAuthError = class _IQAuthError extends Error {
15
+ constructor(code, message, status, cause) {
4
16
  super(message);
5
17
  this.name = "IQAuthError";
6
18
  this.code = code;
7
19
  this.status = status;
8
- this.raw = raw;
20
+ this.cause = cause;
21
+ this.raw = cause;
22
+ }
23
+ /**
24
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
25
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
26
+ */
27
+ static isIQAuthError(value) {
28
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
29
+ }
30
+ /**
31
+ * Type-narrowed code check. Lets callers write
32
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
33
+ * taxonomy without losing the ability to handle server codes via
34
+ * `err.code === "TOKEN_REVOKED"`.
35
+ */
36
+ is(code) {
37
+ return this.code === code;
9
38
  }
10
39
  };
11
40
  var ErrorCodes = {
@@ -46,6 +75,7 @@ var ErrorCodes = {
46
75
  };
47
76
 
48
77
  export {
78
+ IQ_AUTH_ERROR_CODES,
49
79
  IQAuthError,
50
80
  ErrorCodes
51
81
  };