@iqauth/sdk 2.1.0 → 2.3.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 (64) hide show
  1. package/README.md +43 -3
  2. package/dist/browser-session.d.mts +1 -2
  3. package/dist/browser-session.d.ts +1 -2
  4. package/dist/browser-session.js +89 -68
  5. package/dist/browser-session.mjs +2 -1
  6. package/dist/browser.d.mts +2 -2
  7. package/dist/browser.d.ts +2 -2
  8. package/dist/browser.js +69 -7
  9. package/dist/browser.mjs +2 -2
  10. package/dist/{chunk-ZESHDJDU.mjs → chunk-EKTNEZIH.mjs} +5 -8
  11. package/dist/{chunk-JQRTY5MY.mjs → chunk-KGEPDXHU.mjs} +12 -8
  12. package/dist/{chunk-S3M2IXCE.mjs → chunk-RACIPVLD.mjs} +15 -9
  13. package/dist/chunk-UNYDG2L4.mjs +209 -0
  14. package/dist/{chunk-MDUHPQMM.mjs → chunk-W3F4JYGP.mjs} +8 -180
  15. package/dist/chunk-WQWBJSSS.mjs +119 -0
  16. package/dist/cli/index.js +21 -0
  17. package/dist/cli/index.mjs +1 -1
  18. package/dist/{client-DXbHb2ul.d.ts → client-DTX4hNdS.d.ts} +16 -21
  19. package/dist/{client-Dv4v92Mj.d.mts → client-vdh2a9fJ.d.mts} +16 -21
  20. package/dist/{doctor-OHJRZBBT.mjs → doctor-A5E7LSFW.mjs} +2 -1
  21. package/dist/{express-BZmF1llh.d.mts → express-A0-dWEMy.d.mts} +1 -1
  22. package/dist/{express-B4o3P8vK.d.ts → express-Bo_pJKHN.d.ts} +1 -1
  23. package/dist/express.d.mts +75 -5
  24. package/dist/express.d.ts +75 -5
  25. package/dist/express.js +353 -94
  26. package/dist/express.mjs +210 -12
  27. package/dist/fastify.js +153 -88
  28. package/dist/fastify.mjs +10 -9
  29. package/dist/hono.js +152 -88
  30. package/dist/hono.mjs +9 -9
  31. package/dist/index.d.mts +3 -4
  32. package/dist/index.d.ts +3 -4
  33. package/dist/index.js +148 -72
  34. package/dist/index.mjs +16 -12
  35. package/dist/mobile.d.mts +1 -2
  36. package/dist/mobile.d.ts +1 -2
  37. package/dist/mobile.js +89 -68
  38. package/dist/mobile.mjs +2 -1
  39. package/dist/next.d.mts +9 -0
  40. package/dist/next.d.ts +9 -0
  41. package/dist/next.js +164 -1649
  42. package/dist/next.mjs +13 -16
  43. package/dist/{publishableKey-B5DIK81A.d.mts → publishableKey-BaR0HoAH.d.mts} +10 -1
  44. package/dist/{publishableKey-B5DIK81A.d.ts → publishableKey-BaR0HoAH.d.ts} +10 -1
  45. package/dist/react.d.mts +35 -3
  46. package/dist/react.d.ts +35 -3
  47. package/dist/react.js +78 -18
  48. package/dist/react.mjs +14 -2
  49. package/dist/server/handlers.d.mts +2 -0
  50. package/dist/server/handlers.d.ts +2 -0
  51. package/dist/server/handlers.js +72 -17
  52. package/dist/server/handlers.mjs +3 -2
  53. package/dist/server.d.mts +2 -3
  54. package/dist/server.d.ts +2 -3
  55. package/dist/server.js +151 -89
  56. package/dist/server.mjs +7 -6
  57. package/dist/service.d.mts +1 -2
  58. package/dist/service.d.ts +1 -2
  59. package/dist/service.js +89 -68
  60. package/dist/service.mjs +2 -1
  61. package/dist/{signIn-CEMdUAwd.d.mts → signIn-Cd0P4y9d.d.mts} +9 -1
  62. package/dist/{signIn-VRNzlNyG.d.ts → signIn-DKakyzeu.d.ts} +9 -1
  63. package/package.json +3 -2
  64. package/dist/chunk-5WFR6Y33.mjs +0 -59
package/README.md CHANGED
@@ -56,18 +56,34 @@ Create both in one call from the admin Quickstart wizard, or run `npx iqauth ini
56
56
  ### React (browser)
57
57
 
58
58
  ```tsx
59
- import { IQAuthProvider, SignedIn, SignedOut, RedirectToSignIn } from "@iqauth/sdk/react";
59
+ import {
60
+ IQAuthProvider,
61
+ IQAuthLoading,
62
+ IQAuthLoaded,
63
+ SignedIn,
64
+ SignedOut,
65
+ RedirectToSignIn,
66
+ } from "@iqauth/sdk/react";
60
67
 
61
68
  export default function App() {
62
69
  return (
63
70
  <IQAuthProvider publishableKey={import.meta.env.VITE_IQAUTH_PUBLISHABLE_KEY}>
64
- <SignedIn><Dashboard /></SignedIn>
65
- <SignedOut><RedirectToSignIn /></SignedOut>
71
+ <IQAuthLoading><Spinner /></IQAuthLoading>
72
+ <IQAuthLoaded>
73
+ <SignedIn><Dashboard /></SignedIn>
74
+ <SignedOut><RedirectToSignIn /></SignedOut>
75
+ </IQAuthLoaded>
66
76
  </IQAuthProvider>
67
77
  );
68
78
  }
69
79
  ```
70
80
 
81
+ Wrapping the gating components in `<IQAuthLoading/>` / `<IQAuthLoaded/>` is
82
+ the slow-network-safe pattern: until `bootstrap()` finishes, both
83
+ `<SignedIn/>` and `<SignedOut/>` render `null`, which on a slow mobile
84
+ connection is several seconds of blank page. The loading slot fills that
85
+ gap, mirroring Clerk's `<ClerkLoading/>` / `<ClerkLoaded/>`.
86
+
71
87
  Available hooks: `useUser()`, `useSession()`, `useAuth()`, `useOrganization()`. Each returns `{ data, isLoading, error }`.
72
88
  Drop-in components: `<SignIn/>`, `<SignUp/>`, `<UserButton/>`, `<UserProfile/>`, `<OrganizationSwitcher/>`, `<AuthCallback/>`.
73
89
 
@@ -461,6 +477,30 @@ Common codes you'll handle:
461
477
 
462
478
  ---
463
479
 
480
+ ## Don't intercept `/sign-in?code=…`
481
+
482
+ If your app uses an SSO bridge route (e.g. `your-app.com/sign-in` redirects users to `auth.dispositioniq.com`) and that same route is **also** the configured OAuth `redirect_uri`, your bridge logic will fire on the return trip and immediately bounce the user back to the issuer **before** the SDK can exchange the `?code=` for tokens. Symptoms: an infinite redirect loop, or a successful login that the app never sees.
483
+
484
+ **Pick one:**
485
+
486
+ 1. **Recommended — use a dedicated callback path.** Configure `redirect_uri` to point at `/api/iqauth/callback` (the helper route mounted by `iqAuth({...}).attachHelpers(app)` for Express, or by `iqAuth({...})` for Next/Fastify/Hono). Your `/sign-in` route stays purely a "send user to the issuer" bridge.
487
+
488
+ 2. **If you must reuse `/sign-in` as the redirect target,** guard the bridge:
489
+
490
+ ```ts
491
+ app.get("/sign-in", (req, res, next) => {
492
+ // OAuth return trip — let the SDK handle it, don't redirect away.
493
+ if (req.query.code || req.query.error) return next();
494
+ return res.redirect(buildIssuerAuthorizeUrl(req));
495
+ });
496
+ ```
497
+
498
+ …and add `inlineCallback: true` to your `iqAuth({...})` options so a GET handler is mounted on the callback path to complete the exchange and 302 to the final destination.
499
+
500
+ This is the single most common bug we see when teams add IQAuth to an app that already had its own session redirect logic.
501
+
502
+ ---
503
+
464
504
  ## Bundled docs
465
505
 
466
506
  Long-form integration guides ship **inside the npm tarball** at `node_modules/@iqauth/sdk/docs/`. List them with:
@@ -1,7 +1,6 @@
1
1
  import { c as IQAuthBrowserSessionClientConfig, d as SessionUser } from './types-Cxl3bQHt.mjs';
2
- import { I as IQAuthClient } from './client-Dv4v92Mj.mjs';
2
+ import { I as IQAuthClient } from './client-vdh2a9fJ.mjs';
3
3
  export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
4
- import 'jsonwebtoken';
5
4
 
6
5
  declare class BrowserSessionIQAuthClient extends IQAuthClient {
7
6
  constructor(config: Omit<IQAuthBrowserSessionClientConfig, "environment">);
@@ -1,7 +1,6 @@
1
1
  import { c as IQAuthBrowserSessionClientConfig, d as SessionUser } from './types-Cxl3bQHt.js';
2
- import { I as IQAuthClient } from './client-DXbHb2ul.js';
2
+ import { I as IQAuthClient } from './client-DTX4hNdS.js';
3
3
  export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
4
- import 'jsonwebtoken';
5
4
 
6
5
  declare class BrowserSessionIQAuthClient extends IQAuthClient {
7
6
  constructor(config: Omit<IQAuthBrowserSessionClientConfig, "environment">);
@@ -446,8 +446,7 @@ function parseMfaResponse(data, browserSessionMode) {
446
446
  }
447
447
 
448
448
  // src/modules/tokens.ts
449
- var import_crypto = __toESM(require("crypto"));
450
- var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
449
+ var import_jose = require("jose");
451
450
  var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
452
451
  var DEFAULT_TOKEN_ISSUER = [
453
452
  "https://auth.dispositioniq.com",
@@ -460,6 +459,24 @@ var DEFAULT_TOKEN_AUDIENCE = [
460
459
  "iqvalidate"
461
460
  ];
462
461
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
462
+ function decodeProtectedHeader(token) {
463
+ const parts = token.split(".");
464
+ if (parts.length < 2) return null;
465
+ try {
466
+ const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
467
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
468
+ let json;
469
+ if (typeof atob === "function") {
470
+ json = atob(b64);
471
+ } else {
472
+ const { Buffer: Buffer2 } = require("buffer");
473
+ json = Buffer2.from(b64, "base64").toString("utf8");
474
+ }
475
+ return JSON.parse(json);
476
+ } catch {
477
+ return null;
478
+ }
479
+ }
463
480
  var TokensModule = class {
464
481
  constructor(baseUrl, options = {}) {
465
482
  this.jwksCache = null;
@@ -470,49 +487,49 @@ var TokensModule = class {
470
487
  this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
471
488
  }
472
489
  /**
473
- * Verify a JWT access token using RS256 via JWKS from /.well-known/jwks.json.
474
- * Caches JWKS keys for 1 hour. Retries once on unknown `kid`.
475
- *
476
- * @remarks Validates against /.well-known/jwks.json. Issuer, audience, and
477
- * clock tolerance default to client config but can be overridden per call.
490
+ * Verify a JWT access token using RS256/ES256 via JWKS from
491
+ * `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
492
+ * Node, browser, and edge runtimes alike — no `node:crypto` dependency.
493
+ * Caches JWKS for 1 hour and refetches once on unknown `kid`.
478
494
  */
479
495
  async verify(token, options = {}) {
480
- const decoded = import_jsonwebtoken.default.decode(token, { complete: true });
481
- if (!decoded || typeof decoded === "string") {
496
+ const header = decodeProtectedHeader(token);
497
+ if (!header) {
482
498
  throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
483
499
  }
484
- const kid = decoded.header.kid;
500
+ const kid = header.kid;
485
501
  if (!kid) {
486
502
  throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
487
503
  }
488
- let publicKey = await this.getPublicKey(kid);
489
- if (!publicKey) {
490
- await this.refreshJwks();
491
- publicKey = await this.getPublicKey(kid);
504
+ let cache = await this.ensureCache();
505
+ if (!cache.byKid.has(kid)) {
506
+ this.jwksCache = null;
507
+ cache = await this.ensureCache();
492
508
  }
493
- if (!publicKey) {
509
+ if (!cache.byKid.has(kid)) {
494
510
  throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
495
511
  }
496
512
  const issuer = options.issuer ?? this.defaultIssuer;
497
513
  const audience = options.audience ?? this.defaultAudience;
498
514
  const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
499
- const algorithms = options.algorithms ?? ["RS256"];
515
+ const algorithms = options.algorithms ?? ["RS256", "ES256"];
516
+ const verifyOptions = {
517
+ algorithms,
518
+ clockTolerance,
519
+ issuer,
520
+ audience
521
+ };
500
522
  try {
501
- const verifyOptions = {
502
- algorithms,
503
- clockTolerance,
504
- // The jsonwebtoken types insist on tuple types for arrays; runtime
505
- // accepts plain string[] so we cast to satisfy the compiler.
506
- issuer,
507
- audience
508
- };
509
- const verified = import_jsonwebtoken.default.verify(token, publicKey, verifyOptions);
510
- return verified;
523
+ const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
524
+ return payload;
511
525
  } catch (err) {
526
+ if (err instanceof import_jose.errors.JWTExpired) {
527
+ throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
528
+ }
529
+ if (err instanceof import_jose.errors.JOSEError) {
530
+ throw new IQAuthError("TOKEN_INVALID", err.message);
531
+ }
512
532
  if (err instanceof Error) {
513
- if (err.name === "TokenExpiredError") {
514
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
515
- }
516
533
  throw new IQAuthError("TOKEN_INVALID", err.message);
517
534
  }
518
535
  throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
@@ -520,29 +537,40 @@ var TokensModule = class {
520
537
  }
521
538
  /**
522
539
  * Decode a JWT without verification. Returns null if malformed.
523
- *
524
- * @remarks Local decode only — no network call
525
540
  */
526
541
  decode(token) {
527
- const decoded = import_jsonwebtoken.default.decode(token);
528
- return decoded;
542
+ try {
543
+ const parts = token.split(".");
544
+ if (parts.length < 2) return null;
545
+ const payload = parts[1];
546
+ const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
547
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
548
+ let json;
549
+ if (typeof atob === "function") {
550
+ json = atob(b64);
551
+ } else {
552
+ const { Buffer: Buffer2 } = require("buffer");
553
+ json = Buffer2.from(b64, "base64").toString("utf8");
554
+ }
555
+ try {
556
+ json = decodeURIComponent(escape(json));
557
+ } catch {
558
+ }
559
+ const claims = JSON.parse(json);
560
+ if (!claims || typeof claims !== "object") return null;
561
+ return claims;
562
+ } catch {
563
+ return null;
564
+ }
529
565
  }
530
- /**
531
- * Check if a token is expired based on the `exp` claim.
532
- *
533
- * @remarks Local check only — no network call
534
- */
566
+ /** Check if a token is expired based on the `exp` claim. */
535
567
  isExpired(token) {
536
568
  const claims = this.decode(token);
537
569
  if (!claims?.exp) return true;
538
570
  const now = Math.floor(Date.now() / 1e3);
539
571
  return claims.exp <= now;
540
572
  }
541
- /**
542
- * Get the claims from a token without verification.
543
- *
544
- * @remarks Local decode only — no network call
545
- */
573
+ /** Get the claims from a token without verification. */
546
574
  getClaims(token) {
547
575
  const claims = this.decode(token);
548
576
  if (!claims) {
@@ -550,11 +578,15 @@ var TokensModule = class {
550
578
  }
551
579
  return claims;
552
580
  }
553
- async getPublicKey(kid) {
554
- if (!this.jwksCache || Date.now() - this.jwksCache.fetchedAt > JWKS_CACHE_TTL_MS) {
555
- await this.refreshJwks();
581
+ async ensureCache() {
582
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
583
+ return this.jwksCache;
584
+ }
585
+ await this.refreshJwks();
586
+ if (!this.jwksCache) {
587
+ throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
556
588
  }
557
- return this.jwksCache?.keys.get(kid) ?? null;
589
+ return this.jwksCache;
558
590
  }
559
591
  async refreshJwks() {
560
592
  if (this.inFlightRefresh) {
@@ -581,35 +613,24 @@ var TokensModule = class {
581
613
  "Malformed JWKS response: expected { keys: [...] }"
582
614
  );
583
615
  }
584
- const keys = /* @__PURE__ */ new Map();
616
+ const byKid = /* @__PURE__ */ new Set();
585
617
  for (const key of jwks.keys) {
586
- if (!key || typeof key.kid !== "string" || typeof key.n !== "string" || typeof key.e !== "string") {
618
+ 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")) {
587
619
  throw new IQAuthError(
588
620
  "INTERNAL_ERROR",
589
621
  "Malformed JWKS response: key missing required fields"
590
622
  );
591
623
  }
592
- const pem = this.jwkToPem(key);
593
- keys.set(key.kid, pem);
624
+ byKid.add(key.kid);
594
625
  }
595
- this.jwksCache = { keys, fetchedAt: Date.now() };
626
+ const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
627
+ this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
596
628
  } finally {
597
629
  this.inFlightRefresh = null;
598
630
  }
599
631
  })();
600
632
  return this.inFlightRefresh;
601
633
  }
602
- jwkToPem(jwk) {
603
- const keyObject = import_crypto.default.createPublicKey({
604
- key: {
605
- kty: jwk.kty,
606
- n: jwk.n,
607
- e: jwk.e
608
- },
609
- format: "jwk"
610
- });
611
- return keyObject.export({ type: "spki", format: "pem" });
612
- }
613
634
  /** @internal Exposed for testing — clears JWKS cache */
614
635
  clearCache() {
615
636
  this.jwksCache = null;
@@ -817,7 +838,7 @@ var PermissionsModule = class {
817
838
  };
818
839
 
819
840
  // src/modules/oidc.ts
820
- var import_crypto2 = __toESM(require("crypto"));
841
+ var import_crypto = __toESM(require("crypto"));
821
842
  var InMemoryOidcStateStore = class {
822
843
  constructor() {
823
844
  this.map = /* @__PURE__ */ new Map();
@@ -898,12 +919,12 @@ var OidcModule = class {
898
919
  * ready to redirect the user to.
899
920
  */
900
921
  async createAuthRequest(params) {
901
- const codeVerifier = base64UrlEncode(import_crypto2.default.randomBytes(32));
922
+ const codeVerifier = base64UrlEncode(import_crypto.default.randomBytes(32));
902
923
  const codeChallenge = base64UrlEncode(
903
- import_crypto2.default.createHash("sha256").update(codeVerifier).digest()
924
+ import_crypto.default.createHash("sha256").update(codeVerifier).digest()
904
925
  );
905
- const state = base64UrlEncode(import_crypto2.default.randomBytes(16));
906
- const nonce = base64UrlEncode(import_crypto2.default.randomBytes(16));
926
+ const state = base64UrlEncode(import_crypto.default.randomBytes(16));
927
+ const nonce = base64UrlEncode(import_crypto.default.randomBytes(16));
907
928
  await this.stateStore.set(state, {
908
929
  codeVerifier,
909
930
  state,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  IQAuthClient
3
- } from "./chunk-MDUHPQMM.mjs";
3
+ } from "./chunk-W3F4JYGP.mjs";
4
+ import "./chunk-UNYDG2L4.mjs";
4
5
  import {
5
6
  ErrorCodes,
6
7
  IQAuthError
@@ -1,5 +1,5 @@
1
- export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-CEMdUAwd.mjs';
2
- export { K as KeyMode, b as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, a as isSecretKey, p as parsePublishableKey } from './publishableKey-B5DIK81A.mjs';
1
+ export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-Cd0P4y9d.mjs';
2
+ 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';
3
3
  export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
4
4
  import './types-Cxl3bQHt.mjs';
5
5
 
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-VRNzlNyG.js';
2
- export { K as KeyMode, b as ParsedPublishableKey, P as PublishableKeyPayload, e as encodePublishableKey, i as isPublishableKey, a as isSecretKey, p as parsePublishableKey } from './publishableKey-B5DIK81A.js';
1
+ export { C as CallbackResult, S as SessionManager, d as SessionManagerOptions, a as SessionSnapshot, e as SessionStatus, b as SignInOptions, c as SignOutOptions, f as buildSignInUrl, h as handleAuthCallback, r as redirectToSignIn, s as signIn, g as signOut } from './signIn-DKakyzeu.js';
2
+ 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';
3
3
  export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
4
4
  import './types-Cxl3bQHt.js';
5
5
 
package/dist/browser.js CHANGED
@@ -116,6 +116,18 @@ function encodePublishableKey(mode, payload) {
116
116
  if (mode !== "test" && mode !== "live") throw new Error(`Invalid mode: ${mode}`);
117
117
  return `pk_${mode}_${b64urlEncode(JSON.stringify(payload))}`;
118
118
  }
119
+ function isValidIssuerUrl(iss) {
120
+ if (typeof iss !== "string" || iss.length === 0) return false;
121
+ if (!iss.startsWith("http://") && !iss.startsWith("https://")) return false;
122
+ try {
123
+ const u = new URL(iss);
124
+ if (u.protocol !== "http:" && u.protocol !== "https:") return false;
125
+ if (!u.hostname) return false;
126
+ return true;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
119
131
  function parsePublishableKey(raw) {
120
132
  if (typeof raw !== "string") return null;
121
133
  const m = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
@@ -126,11 +138,55 @@ function parsePublishableKey(raw) {
126
138
  if (typeof json.iss !== "string" || typeof json.appId !== "string" || typeof json.tenantId !== "string" || typeof json.kid !== "string") {
127
139
  return null;
128
140
  }
141
+ if (!isValidIssuerUrl(json.iss)) return null;
129
142
  return { mode: m[1], iss: json.iss, appId: json.appId, tenantId: json.tenantId, kid: json.kid, raw };
130
143
  } catch {
131
144
  return null;
132
145
  }
133
146
  }
147
+ function assertPublishableKey(raw, opts) {
148
+ const ctx = opts?.context ? `${opts.context}: ` : "";
149
+ if (typeof raw !== "string" || raw.length === 0) {
150
+ throw new IQAuthError(
151
+ "CONFIG_INVALID",
152
+ `${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.`
153
+ );
154
+ }
155
+ const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
156
+ if (!shapeMatch) {
157
+ throw new IQAuthError(
158
+ "CONFIG_INVALID",
159
+ `${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.`
160
+ );
161
+ }
162
+ let decoded;
163
+ try {
164
+ decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
165
+ } catch {
166
+ throw new IQAuthError(
167
+ "CONFIG_INVALID",
168
+ `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
169
+ );
170
+ }
171
+ if (!isPublishableKeyPayload(decoded)) {
172
+ throw new IQAuthError(
173
+ "CONFIG_INVALID",
174
+ `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
175
+ );
176
+ }
177
+ if (!isValidIssuerUrl(decoded.iss)) {
178
+ throw new IQAuthError(
179
+ "CONFIG_INVALID",
180
+ `${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.`
181
+ );
182
+ }
183
+ return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
184
+ }
185
+ function isPublishableKeyPayload(value) {
186
+ if (!value || typeof value !== "object") return false;
187
+ const v = value;
188
+ return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
189
+ }
134
190
  function isPublishableKey(raw) {
135
191
  return typeof raw === "string" && /^pk_(test|live)_/.test(raw);
136
192
  }
@@ -258,12 +314,7 @@ var SessionManager = class {
258
314
  this.remoteRefreshWaiters = [];
259
315
  /** Active claims by other tabs (keyed by source tabId). */
260
316
  this.foreignClaim = null;
261
- const parsed = parsePublishableKey(options.publishableKey);
262
- if (!parsed) {
263
- throw new Error(
264
- `Invalid IQAuth publishable key. Expected pk_test_\u2026 or pk_live_\u2026 (got ${options.publishableKey?.slice(0, 12) ?? "<empty>"}\u2026).`
265
- );
266
- }
317
+ const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
267
318
  this.key = parsed;
268
319
  const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
269
320
  this.issuer = inferred.replace(/\/+$/, "");
@@ -664,6 +715,7 @@ async function createPkcePair() {
664
715
  // src/browser/signIn.ts
665
716
  var DEFAULT_SIGN_IN_PATH = "/sign-in";
666
717
  var DEFAULT_LOGOUT_PATH = "/api/v1/auth/logout";
718
+ var DEFAULT_SSO_LOGOUT_PATH = "/oidc/sso-logout";
667
719
  var DEFAULT_TOKEN_PATH = "/oidc/token";
668
720
  var DEFAULT_CALLBACK_PATH = "/auth/callback";
669
721
  function defaultRedirectUri() {
@@ -762,11 +814,21 @@ async function handleAuthCallback(manager, options = {}) {
762
814
  }
763
815
  async function signOut(manager, opts = {}) {
764
816
  if (!opts.localOnly) {
817
+ const issuer = manager.issuerUrl.replace(/\/$/, "");
765
818
  try {
766
- const url = `${manager.issuerUrl}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
819
+ const url = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
767
820
  await manager.fetch(url, { method: "POST" }).catch(() => void 0);
768
821
  } catch {
769
822
  }
823
+ if (opts.endSsoSession !== false) {
824
+ try {
825
+ await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
826
+ method: "POST",
827
+ credentials: "include"
828
+ }).catch(() => void 0);
829
+ } catch {
830
+ }
831
+ }
770
832
  }
771
833
  clearCookie(REFRESH_COOKIE);
772
834
  manager.signOutLocal();
package/dist/browser.mjs CHANGED
@@ -12,13 +12,13 @@ import {
12
12
  setCookie,
13
13
  signIn,
14
14
  signOut
15
- } from "./chunk-S3M2IXCE.mjs";
15
+ } from "./chunk-RACIPVLD.mjs";
16
16
  import {
17
17
  encodePublishableKey,
18
18
  isPublishableKey,
19
19
  isSecretKey,
20
20
  parsePublishableKey
21
- } from "./chunk-5WFR6Y33.mjs";
21
+ } from "./chunk-WQWBJSSS.mjs";
22
22
  import {
23
23
  ErrorCodes,
24
24
  IQAuthError
@@ -1,9 +1,9 @@
1
- import {
2
- parsePublishableKey
3
- } from "./chunk-5WFR6Y33.mjs";
4
1
  import {
5
2
  IQAuthClient
6
- } from "./chunk-MDUHPQMM.mjs";
3
+ } from "./chunk-W3F4JYGP.mjs";
4
+ import {
5
+ assertPublishableKey
6
+ } from "./chunk-WQWBJSSS.mjs";
7
7
  import {
8
8
  IQAuthError
9
9
  } from "./chunk-6I6RM4MN.mjs";
@@ -44,10 +44,7 @@ function readCookie(req, name) {
44
44
  return void 0;
45
45
  }
46
46
  function clientFromPublishableKey(opts) {
47
- const parsed = parsePublishableKey(opts.publishableKey);
48
- if (!parsed) {
49
- throw new Error("iqAuthMiddleware: invalid publishable key");
50
- }
47
+ const parsed = assertPublishableKey(opts.publishableKey, { context: "iqAuthMiddleware" });
51
48
  const issuer = (opts.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
52
49
  return new IQAuthClient({ baseUrl: issuer, environment: "server" });
53
50
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
- parsePublishableKey
3
- } from "./chunk-5WFR6Y33.mjs";
2
+ assertPublishableKey
3
+ } from "./chunk-WQWBJSSS.mjs";
4
4
 
5
5
  // src/server/handlers.ts
6
6
  var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
@@ -22,12 +22,7 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
22
22
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
23
23
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
24
24
  function resolve(config) {
25
- const parsed = parsePublishableKey(config.publishableKey);
26
- if (!parsed) {
27
- throw new Error(
28
- "@iqauth/sdk: invalid publishable key passed to iqAuth helpers (expected pk_test_\u2026 or pk_live_\u2026)"
29
- );
30
- }
25
+ const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
31
26
  const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
32
27
  return {
33
28
  publishableKey: config.publishableKey,
@@ -198,6 +193,15 @@ async function handleSignout(config, input) {
198
193
  } catch {
199
194
  }
200
195
  }
196
+ if (input.endSsoSession !== false && input.ssoCookieHeader) {
197
+ try {
198
+ await cfg.fetchImpl(`${cfg.issuer}/oidc/sso-logout`, {
199
+ method: "POST",
200
+ headers: { Cookie: input.ssoCookieHeader }
201
+ });
202
+ } catch {
203
+ }
204
+ }
201
205
  return {
202
206
  status: 200,
203
207
  body: { success: true, data: { signedOut: true } },
@@ -1,6 +1,6 @@
1
1
  import {
2
- parsePublishableKey
3
- } from "./chunk-5WFR6Y33.mjs";
2
+ assertPublishableKey
3
+ } from "./chunk-WQWBJSSS.mjs";
4
4
  import {
5
5
  IQAuthError
6
6
  } from "./chunk-6I6RM4MN.mjs";
@@ -125,12 +125,7 @@ var SessionManager = class {
125
125
  this.remoteRefreshWaiters = [];
126
126
  /** Active claims by other tabs (keyed by source tabId). */
127
127
  this.foreignClaim = null;
128
- const parsed = parsePublishableKey(options.publishableKey);
129
- if (!parsed) {
130
- throw new Error(
131
- `Invalid IQAuth publishable key. Expected pk_test_\u2026 or pk_live_\u2026 (got ${options.publishableKey?.slice(0, 12) ?? "<empty>"}\u2026).`
132
- );
133
- }
128
+ const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
134
129
  this.key = parsed;
135
130
  const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
136
131
  this.issuer = inferred.replace(/\/+$/, "");
@@ -531,6 +526,7 @@ async function createPkcePair() {
531
526
  // src/browser/signIn.ts
532
527
  var DEFAULT_SIGN_IN_PATH = "/sign-in";
533
528
  var DEFAULT_LOGOUT_PATH = "/api/v1/auth/logout";
529
+ var DEFAULT_SSO_LOGOUT_PATH = "/oidc/sso-logout";
534
530
  var DEFAULT_TOKEN_PATH = "/oidc/token";
535
531
  var DEFAULT_CALLBACK_PATH = "/auth/callback";
536
532
  function defaultRedirectUri() {
@@ -629,11 +625,21 @@ async function handleAuthCallback(manager, options = {}) {
629
625
  }
630
626
  async function signOut(manager, opts = {}) {
631
627
  if (!opts.localOnly) {
628
+ const issuer = manager.issuerUrl.replace(/\/$/, "");
632
629
  try {
633
- const url = `${manager.issuerUrl}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
630
+ const url = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
634
631
  await manager.fetch(url, { method: "POST" }).catch(() => void 0);
635
632
  } catch {
636
633
  }
634
+ if (opts.endSsoSession !== false) {
635
+ try {
636
+ await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
637
+ method: "POST",
638
+ credentials: "include"
639
+ }).catch(() => void 0);
640
+ } catch {
641
+ }
642
+ }
637
643
  }
638
644
  clearCookie(REFRESH_COOKIE);
639
645
  manager.signOutLocal();