@iqauth/sdk 2.0.1 → 2.0.3

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.
@@ -1,4 +1,4 @@
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-C8f6qVjD.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-CEMdUAwd.mjs';
2
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';
3
3
  export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
4
4
  import './types-Cxl3bQHt.mjs';
package/dist/browser.d.ts CHANGED
@@ -1,4 +1,4 @@
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-Cy2lbEXb.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-VRNzlNyG.js';
2
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';
3
3
  export { a as ErrorCode, E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
4
4
  import './types-Cxl3bQHt.js';
package/dist/browser.js CHANGED
@@ -270,8 +270,9 @@ var SessionManager = class {
270
270
  this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
271
271
  this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
272
272
  this.useCookies = options.useCookies ?? true;
273
- this.proactiveRefresh = options.proactiveRefresh ?? true;
274
- this.tokenStore = options.tokenStore ?? (this.useCookies ? defaultCookieStore() : NO_OP_STORE);
273
+ this.serverManagedSession = options.serverManagedSession ?? false;
274
+ this.proactiveRefresh = this.serverManagedSession ? false : options.proactiveRefresh ?? true;
275
+ this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore() : NO_OP_STORE);
275
276
  this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
276
277
  this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
277
278
  throw new Error("global fetch is not available; pass fetchImpl");
@@ -315,6 +316,40 @@ var SessionManager = class {
315
316
  async bootstrap() {
316
317
  if (this.bootstrapped) return;
317
318
  this.bootstrapped = true;
319
+ if (this.serverManagedSession) {
320
+ try {
321
+ const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
322
+ method: "GET",
323
+ credentials: "include",
324
+ headers: { Accept: "application/json" }
325
+ });
326
+ if (!res.ok) {
327
+ this.setStatus("unauthenticated");
328
+ return;
329
+ }
330
+ const body = await res.json().catch(() => ({}));
331
+ const data = body?.data;
332
+ const user = data?.user ?? claimsToSessionUser(data?.claims ?? null);
333
+ if (!user) {
334
+ this.setStatus("unauthenticated");
335
+ return;
336
+ }
337
+ this.update({
338
+ status: "authenticated",
339
+ accessToken: null,
340
+ user,
341
+ claims: data?.claims ?? null,
342
+ tenantId: data?.tenantId ?? user.tenantId ?? this.key.tenantId,
343
+ error: null,
344
+ version: this.snapshot.version + 1
345
+ });
346
+ this.broadcast("session:update");
347
+ return;
348
+ } catch {
349
+ this.setStatus("unauthenticated");
350
+ return;
351
+ }
352
+ }
318
353
  const stored = await Promise.resolve(this.tokenStore.read());
319
354
  if (!stored) {
320
355
  this.setStatus("unauthenticated");
package/dist/browser.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  setCookie,
13
13
  signIn,
14
14
  signOut
15
- } from "./chunk-YDO2RDWQ.mjs";
15
+ } from "./chunk-S3M2IXCE.mjs";
16
16
  import {
17
17
  encodePublishableKey,
18
18
  isPublishableKey,
@@ -3,6 +3,22 @@ import {
3
3
  } from "./chunk-5WFR6Y33.mjs";
4
4
 
5
5
  // src/server/handlers.ts
6
+ var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
7
+ "TOKEN_REVOKED",
8
+ "SESSION_REVOKED",
9
+ "INVALID_GRANT",
10
+ "invalid_grant",
11
+ "USER_DEACTIVATED",
12
+ "USER_DISABLED",
13
+ "TENANT_SUSPENDED"
14
+ ]);
15
+ function shouldClearCookiesOnFailure(policy, status, errorCode) {
16
+ if (policy === "always") return true;
17
+ if (policy === "never") return false;
18
+ if (status === 410) return true;
19
+ if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
20
+ return false;
21
+ }
6
22
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
7
23
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
8
24
  function resolve(config) {
@@ -30,7 +46,8 @@ function resolve(config) {
30
46
  throw new Error("global fetch is unavailable; pass fetchImpl");
31
47
  })),
32
48
  appId: parsed.appId,
33
- tenantId: parsed.tenantId
49
+ tenantId: parsed.tenantId,
50
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
34
51
  };
35
52
  }
36
53
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -126,7 +143,7 @@ async function handleRefresh(config, input) {
126
143
  return {
127
144
  status: 401,
128
145
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
129
- cookies: clearCookies(cfg)
146
+ cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
130
147
  };
131
148
  }
132
149
  const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
@@ -136,16 +153,23 @@ async function handleRefresh(config, input) {
136
153
  });
137
154
  const json = await res.json().catch(() => ({}));
138
155
  if (!res.ok || !json.success || !json.data?.accessToken) {
156
+ const status = res.status || 401;
157
+ const errorCode = json.error?.code || "TOKEN_INVALID";
158
+ const shouldClear = shouldClearCookiesOnFailure(
159
+ cfg.clearCookiesOnRefreshFailure,
160
+ status,
161
+ errorCode
162
+ );
139
163
  return {
140
- status: res.status || 401,
164
+ status,
141
165
  body: {
142
166
  success: false,
143
167
  error: {
144
- code: json.error?.code || "TOKEN_INVALID",
168
+ code: errorCode,
145
169
  message: json.error?.message || "Refresh failed"
146
170
  }
147
171
  },
148
- cookies: clearCookies(cfg)
172
+ cookies: shouldClear ? clearCookies(cfg) : []
149
173
  };
150
174
  }
151
175
  const cookies = [
@@ -137,8 +137,9 @@ var SessionManager = class {
137
137
  this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
138
138
  this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
139
139
  this.useCookies = options.useCookies ?? true;
140
- this.proactiveRefresh = options.proactiveRefresh ?? true;
141
- this.tokenStore = options.tokenStore ?? (this.useCookies ? defaultCookieStore() : NO_OP_STORE);
140
+ this.serverManagedSession = options.serverManagedSession ?? false;
141
+ this.proactiveRefresh = this.serverManagedSession ? false : options.proactiveRefresh ?? true;
142
+ this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore() : NO_OP_STORE);
142
143
  this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
143
144
  this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
144
145
  throw new Error("global fetch is not available; pass fetchImpl");
@@ -182,6 +183,40 @@ var SessionManager = class {
182
183
  async bootstrap() {
183
184
  if (this.bootstrapped) return;
184
185
  this.bootstrapped = true;
186
+ if (this.serverManagedSession) {
187
+ try {
188
+ const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
189
+ method: "GET",
190
+ credentials: "include",
191
+ headers: { Accept: "application/json" }
192
+ });
193
+ if (!res.ok) {
194
+ this.setStatus("unauthenticated");
195
+ return;
196
+ }
197
+ const body = await res.json().catch(() => ({}));
198
+ const data = body?.data;
199
+ const user = data?.user ?? claimsToSessionUser(data?.claims ?? null);
200
+ if (!user) {
201
+ this.setStatus("unauthenticated");
202
+ return;
203
+ }
204
+ this.update({
205
+ status: "authenticated",
206
+ accessToken: null,
207
+ user,
208
+ claims: data?.claims ?? null,
209
+ tenantId: data?.tenantId ?? user.tenantId ?? this.key.tenantId,
210
+ error: null,
211
+ version: this.snapshot.version + 1
212
+ });
213
+ this.broadcast("session:update");
214
+ return;
215
+ } catch {
216
+ this.setStatus("unauthenticated");
217
+ return;
218
+ }
219
+ }
185
220
  const stored = await Promise.resolve(this.tokenStore.read());
186
221
  if (!stored) {
187
222
  this.setStatus("unauthenticated");
package/dist/express.js CHANGED
@@ -1980,6 +1980,22 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
1980
1980
  }
1981
1981
 
1982
1982
  // src/server/handlers.ts
1983
+ var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
1984
+ "TOKEN_REVOKED",
1985
+ "SESSION_REVOKED",
1986
+ "INVALID_GRANT",
1987
+ "invalid_grant",
1988
+ "USER_DEACTIVATED",
1989
+ "USER_DISABLED",
1990
+ "TENANT_SUSPENDED"
1991
+ ]);
1992
+ function shouldClearCookiesOnFailure(policy, status, errorCode) {
1993
+ if (policy === "always") return true;
1994
+ if (policy === "never") return false;
1995
+ if (status === 410) return true;
1996
+ if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
1997
+ return false;
1998
+ }
1983
1999
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
1984
2000
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
1985
2001
  function resolve(config) {
@@ -2007,7 +2023,8 @@ function resolve(config) {
2007
2023
  throw new Error("global fetch is unavailable; pass fetchImpl");
2008
2024
  })),
2009
2025
  appId: parsed.appId,
2010
- tenantId: parsed.tenantId
2026
+ tenantId: parsed.tenantId,
2027
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
2011
2028
  };
2012
2029
  }
2013
2030
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -2093,7 +2110,7 @@ async function handleRefresh(config, input) {
2093
2110
  return {
2094
2111
  status: 401,
2095
2112
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
2096
- cookies: clearCookies(cfg)
2113
+ cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
2097
2114
  };
2098
2115
  }
2099
2116
  const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
@@ -2103,16 +2120,23 @@ async function handleRefresh(config, input) {
2103
2120
  });
2104
2121
  const json = await res.json().catch(() => ({}));
2105
2122
  if (!res.ok || !json.success || !json.data?.accessToken) {
2123
+ const status = res.status || 401;
2124
+ const errorCode = json.error?.code || "TOKEN_INVALID";
2125
+ const shouldClear = shouldClearCookiesOnFailure(
2126
+ cfg.clearCookiesOnRefreshFailure,
2127
+ status,
2128
+ errorCode
2129
+ );
2106
2130
  return {
2107
- status: res.status || 401,
2131
+ status,
2108
2132
  body: {
2109
2133
  success: false,
2110
2134
  error: {
2111
- code: json.error?.code || "TOKEN_INVALID",
2135
+ code: errorCode,
2112
2136
  message: json.error?.message || "Refresh failed"
2113
2137
  }
2114
2138
  },
2115
- cookies: clearCookies(cfg)
2139
+ cookies: shouldClear ? clearCookies(cfg) : []
2116
2140
  };
2117
2141
  }
2118
2142
  const cookies = [
package/dist/express.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  handleCallback,
7
7
  handleRefresh,
8
8
  handleSignout
9
- } from "./chunk-5HF3OBNO.mjs";
9
+ } from "./chunk-JQRTY5MY.mjs";
10
10
  import {
11
11
  parsePublishableKey
12
12
  } from "./chunk-5WFR6Y33.mjs";
package/dist/fastify.js CHANGED
@@ -1781,6 +1781,22 @@ function parsePublishableKey(raw) {
1781
1781
  }
1782
1782
 
1783
1783
  // src/server/handlers.ts
1784
+ var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
1785
+ "TOKEN_REVOKED",
1786
+ "SESSION_REVOKED",
1787
+ "INVALID_GRANT",
1788
+ "invalid_grant",
1789
+ "USER_DEACTIVATED",
1790
+ "USER_DISABLED",
1791
+ "TENANT_SUSPENDED"
1792
+ ]);
1793
+ function shouldClearCookiesOnFailure(policy, status, errorCode) {
1794
+ if (policy === "always") return true;
1795
+ if (policy === "never") return false;
1796
+ if (status === 410) return true;
1797
+ if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
1798
+ return false;
1799
+ }
1784
1800
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
1785
1801
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
1786
1802
  function resolve(config) {
@@ -1808,7 +1824,8 @@ function resolve(config) {
1808
1824
  throw new Error("global fetch is unavailable; pass fetchImpl");
1809
1825
  })),
1810
1826
  appId: parsed.appId,
1811
- tenantId: parsed.tenantId
1827
+ tenantId: parsed.tenantId,
1828
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
1812
1829
  };
1813
1830
  }
1814
1831
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -1904,7 +1921,7 @@ async function handleRefresh(config, input) {
1904
1921
  return {
1905
1922
  status: 401,
1906
1923
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
1907
- cookies: clearCookies(cfg)
1924
+ cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
1908
1925
  };
1909
1926
  }
1910
1927
  const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
@@ -1914,16 +1931,23 @@ async function handleRefresh(config, input) {
1914
1931
  });
1915
1932
  const json = await res.json().catch(() => ({}));
1916
1933
  if (!res.ok || !json.success || !json.data?.accessToken) {
1934
+ const status = res.status || 401;
1935
+ const errorCode = json.error?.code || "TOKEN_INVALID";
1936
+ const shouldClear = shouldClearCookiesOnFailure(
1937
+ cfg.clearCookiesOnRefreshFailure,
1938
+ status,
1939
+ errorCode
1940
+ );
1917
1941
  return {
1918
- status: res.status || 401,
1942
+ status,
1919
1943
  body: {
1920
1944
  success: false,
1921
1945
  error: {
1922
- code: json.error?.code || "TOKEN_INVALID",
1946
+ code: errorCode,
1923
1947
  message: json.error?.message || "Refresh failed"
1924
1948
  }
1925
1949
  },
1926
- cookies: clearCookies(cfg)
1950
+ cookies: shouldClear ? clearCookies(cfg) : []
1927
1951
  };
1928
1952
  }
1929
1953
  const cookies = [
package/dist/fastify.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  handleRefresh,
4
4
  handleSignout,
5
5
  serializeCookie
6
- } from "./chunk-5HF3OBNO.mjs";
6
+ } from "./chunk-JQRTY5MY.mjs";
7
7
  import {
8
8
  parsePublishableKey
9
9
  } from "./chunk-5WFR6Y33.mjs";
package/dist/hono.js CHANGED
@@ -1780,6 +1780,22 @@ function parsePublishableKey(raw) {
1780
1780
  }
1781
1781
 
1782
1782
  // src/server/handlers.ts
1783
+ var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
1784
+ "TOKEN_REVOKED",
1785
+ "SESSION_REVOKED",
1786
+ "INVALID_GRANT",
1787
+ "invalid_grant",
1788
+ "USER_DEACTIVATED",
1789
+ "USER_DISABLED",
1790
+ "TENANT_SUSPENDED"
1791
+ ]);
1792
+ function shouldClearCookiesOnFailure(policy, status, errorCode) {
1793
+ if (policy === "always") return true;
1794
+ if (policy === "never") return false;
1795
+ if (status === 410) return true;
1796
+ if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
1797
+ return false;
1798
+ }
1783
1799
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
1784
1800
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
1785
1801
  function resolve(config) {
@@ -1807,7 +1823,8 @@ function resolve(config) {
1807
1823
  throw new Error("global fetch is unavailable; pass fetchImpl");
1808
1824
  })),
1809
1825
  appId: parsed.appId,
1810
- tenantId: parsed.tenantId
1826
+ tenantId: parsed.tenantId,
1827
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
1811
1828
  };
1812
1829
  }
1813
1830
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -1903,7 +1920,7 @@ async function handleRefresh(config, input) {
1903
1920
  return {
1904
1921
  status: 401,
1905
1922
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
1906
- cookies: clearCookies(cfg)
1923
+ cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
1907
1924
  };
1908
1925
  }
1909
1926
  const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
@@ -1913,16 +1930,23 @@ async function handleRefresh(config, input) {
1913
1930
  });
1914
1931
  const json = await res.json().catch(() => ({}));
1915
1932
  if (!res.ok || !json.success || !json.data?.accessToken) {
1933
+ const status = res.status || 401;
1934
+ const errorCode = json.error?.code || "TOKEN_INVALID";
1935
+ const shouldClear = shouldClearCookiesOnFailure(
1936
+ cfg.clearCookiesOnRefreshFailure,
1937
+ status,
1938
+ errorCode
1939
+ );
1916
1940
  return {
1917
- status: res.status || 401,
1941
+ status,
1918
1942
  body: {
1919
1943
  success: false,
1920
1944
  error: {
1921
- code: json.error?.code || "TOKEN_INVALID",
1945
+ code: errorCode,
1922
1946
  message: json.error?.message || "Refresh failed"
1923
1947
  }
1924
1948
  },
1925
- cookies: clearCookies(cfg)
1949
+ cookies: shouldClear ? clearCookies(cfg) : []
1926
1950
  };
1927
1951
  }
1928
1952
  const cookies = [
package/dist/hono.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  handleRefresh,
4
4
  handleSignout,
5
5
  serializeCookie
6
- } from "./chunk-5HF3OBNO.mjs";
6
+ } from "./chunk-JQRTY5MY.mjs";
7
7
  import {
8
8
  parsePublishableKey
9
9
  } from "./chunk-5WFR6Y33.mjs";
package/dist/next.js CHANGED
@@ -66,6 +66,22 @@ function parsePublishableKey(raw) {
66
66
  }
67
67
 
68
68
  // src/server/handlers.ts
69
+ var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
70
+ "TOKEN_REVOKED",
71
+ "SESSION_REVOKED",
72
+ "INVALID_GRANT",
73
+ "invalid_grant",
74
+ "USER_DEACTIVATED",
75
+ "USER_DISABLED",
76
+ "TENANT_SUSPENDED"
77
+ ]);
78
+ function shouldClearCookiesOnFailure(policy, status, errorCode) {
79
+ if (policy === "always") return true;
80
+ if (policy === "never") return false;
81
+ if (status === 410) return true;
82
+ if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
83
+ return false;
84
+ }
69
85
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
70
86
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
71
87
  function resolve(config) {
@@ -93,7 +109,8 @@ function resolve(config) {
93
109
  throw new Error("global fetch is unavailable; pass fetchImpl");
94
110
  })),
95
111
  appId: parsed.appId,
96
- tenantId: parsed.tenantId
112
+ tenantId: parsed.tenantId,
113
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
97
114
  };
98
115
  }
99
116
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -189,7 +206,7 @@ async function handleRefresh(config, input) {
189
206
  return {
190
207
  status: 401,
191
208
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
192
- cookies: clearCookies(cfg)
209
+ cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
193
210
  };
194
211
  }
195
212
  const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
@@ -199,16 +216,23 @@ async function handleRefresh(config, input) {
199
216
  });
200
217
  const json = await res.json().catch(() => ({}));
201
218
  if (!res.ok || !json.success || !json.data?.accessToken) {
219
+ const status = res.status || 401;
220
+ const errorCode = json.error?.code || "TOKEN_INVALID";
221
+ const shouldClear = shouldClearCookiesOnFailure(
222
+ cfg.clearCookiesOnRefreshFailure,
223
+ status,
224
+ errorCode
225
+ );
202
226
  return {
203
- status: res.status || 401,
227
+ status,
204
228
  body: {
205
229
  success: false,
206
230
  error: {
207
- code: json.error?.code || "TOKEN_INVALID",
231
+ code: errorCode,
208
232
  message: json.error?.message || "Refresh failed"
209
233
  }
210
234
  },
211
- cookies: clearCookies(cfg)
235
+ cookies: shouldClear ? clearCookies(cfg) : []
212
236
  };
213
237
  }
214
238
  const cookies = [
@@ -2057,7 +2081,7 @@ async function getAuth(options) {
2057
2081
  /* webpackIgnore: true */
2058
2082
  specifier
2059
2083
  );
2060
- cookieJar = mod.cookies ? mod.cookies() : null;
2084
+ cookieJar = mod.cookies ? await mod.cookies() : null;
2061
2085
  } catch {
2062
2086
  cookieJar = null;
2063
2087
  }
package/dist/next.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  handleRefresh,
4
4
  handleSignout,
5
5
  serializeCookie
6
- } from "./chunk-5HF3OBNO.mjs";
6
+ } from "./chunk-JQRTY5MY.mjs";
7
7
  import {
8
8
  parsePublishableKey
9
9
  } from "./chunk-5WFR6Y33.mjs";
@@ -110,7 +110,7 @@ async function getAuth(options) {
110
110
  /* webpackIgnore: true */
111
111
  specifier
112
112
  );
113
- cookieJar = mod.cookies ? mod.cookies() : null;
113
+ cookieJar = mod.cookies ? await mod.cookies() : null;
114
114
  } catch {
115
115
  cookieJar = null;
116
116
  }
package/dist/react.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import { ReactNode } from 'react';
4
- import { S as SessionManager, a as SessionSnapshot, b as SignInOptions, c as SignOutOptions, C as CallbackResult } from './signIn-C8f6qVjD.mjs';
4
+ import { S as SessionManager, a as SessionSnapshot, b as SignInOptions, c as SignOutOptions, C as CallbackResult } from './signIn-CEMdUAwd.mjs';
5
5
  import { d as SessionUser, J as JwtClaims } from './types-Cxl3bQHt.mjs';
6
6
  import './publishableKey-B5DIK81A.mjs';
7
7
 
package/dist/react.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import { ReactNode } from 'react';
4
- import { S as SessionManager, a as SessionSnapshot, b as SignInOptions, c as SignOutOptions, C as CallbackResult } from './signIn-Cy2lbEXb.js';
4
+ import { S as SessionManager, a as SessionSnapshot, b as SignInOptions, c as SignOutOptions, C as CallbackResult } from './signIn-VRNzlNyG.js';
5
5
  import { d as SessionUser, J as JwtClaims } from './types-Cxl3bQHt.js';
6
6
  import './publishableKey-B5DIK81A.js';
7
7
 
package/dist/react.js CHANGED
@@ -215,8 +215,9 @@ var SessionManager = class {
215
215
  this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
216
216
  this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
217
217
  this.useCookies = options.useCookies ?? true;
218
- this.proactiveRefresh = options.proactiveRefresh ?? true;
219
- this.tokenStore = options.tokenStore ?? (this.useCookies ? defaultCookieStore() : NO_OP_STORE);
218
+ this.serverManagedSession = options.serverManagedSession ?? false;
219
+ this.proactiveRefresh = this.serverManagedSession ? false : options.proactiveRefresh ?? true;
220
+ this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore() : NO_OP_STORE);
220
221
  this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
221
222
  this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
222
223
  throw new Error("global fetch is not available; pass fetchImpl");
@@ -260,6 +261,40 @@ var SessionManager = class {
260
261
  async bootstrap() {
261
262
  if (this.bootstrapped) return;
262
263
  this.bootstrapped = true;
264
+ if (this.serverManagedSession) {
265
+ try {
266
+ const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
267
+ method: "GET",
268
+ credentials: "include",
269
+ headers: { Accept: "application/json" }
270
+ });
271
+ if (!res.ok) {
272
+ this.setStatus("unauthenticated");
273
+ return;
274
+ }
275
+ const body = await res.json().catch(() => ({}));
276
+ const data = body?.data;
277
+ const user = data?.user ?? claimsToSessionUser(data?.claims ?? null);
278
+ if (!user) {
279
+ this.setStatus("unauthenticated");
280
+ return;
281
+ }
282
+ this.update({
283
+ status: "authenticated",
284
+ accessToken: null,
285
+ user,
286
+ claims: data?.claims ?? null,
287
+ tenantId: data?.tenantId ?? user.tenantId ?? this.key.tenantId,
288
+ error: null,
289
+ version: this.snapshot.version + 1
290
+ });
291
+ this.broadcast("session:update");
292
+ return;
293
+ } catch {
294
+ this.setStatus("unauthenticated");
295
+ return;
296
+ }
297
+ }
263
298
  const stored = await Promise.resolve(this.tokenStore.read());
264
299
  if (!stored) {
265
300
  this.setStatus("unauthenticated");
package/dist/react.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  redirectToSignIn,
5
5
  signIn,
6
6
  signOut
7
- } from "./chunk-YDO2RDWQ.mjs";
7
+ } from "./chunk-S3M2IXCE.mjs";
8
8
  import "./chunk-5WFR6Y33.mjs";
9
9
  import "./chunk-6I6RM4MN.mjs";
10
10
  import "./chunk-Y6FXYEAI.mjs";
@@ -63,14 +63,33 @@ interface IQAuthHelperConfig {
63
63
  logoutPath?: string;
64
64
  /** Optional fetch implementation override. */
65
65
  fetchImpl?: typeof fetch;
66
+ /**
67
+ * Policy for clearing the access + refresh cookies when `/refresh` fails.
68
+ *
69
+ * - `"terminal-only"` (default, recommended): only clear cookies when the
70
+ * issuer indicates the session is unrecoverable
71
+ * (`TOKEN_REVOKED`, `SESSION_REVOKED`, `INVALID_GRANT`,
72
+ * `USER_DEACTIVATED`, or HTTP 410 Gone). Transient failures
73
+ * (`TOKEN_INVALID` from a rotated-out token, `TOKEN_EXPIRED`, network
74
+ * errors, 5xx) leave cookies intact so the next legitimate request can
75
+ * either succeed against a still-valid access cookie or be redirected
76
+ * cleanly to sign-in by the middleware. Fixes the multi-tab /
77
+ * proactive-refresh race that previously silently signed users out.
78
+ * - `"always"`: pre-2.0.3 behavior — wipe both cookies on any non-2xx.
79
+ * Use only if you have an external reason to depend on the old semantics.
80
+ * - `"never"`: leave cookies untouched on every failure path. Suitable for
81
+ * apps that manage cookie lifecycle entirely outside the SDK helpers.
82
+ */
83
+ clearCookiesOnRefreshFailure?: "terminal-only" | "always" | "never";
66
84
  }
67
- interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl">> {
85
+ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure">> {
68
86
  secretKey?: string;
69
87
  cookieDomain?: string;
70
88
  issuer: string;
71
89
  fetchImpl: typeof fetch;
72
90
  appId: string;
73
91
  tenantId: string;
92
+ clearCookiesOnRefreshFailure: "terminal-only" | "always" | "never";
74
93
  }
75
94
  /**
76
95
  * Serialize a cookie directive to a Set-Cookie header value. Adapters that
@@ -84,7 +103,15 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
84
103
  codeVerifier?: string;
85
104
  redirectUri?: string;
86
105
  }): Promise<HandlerResponse>;
87
- /** POST /api/iqauth/refresh — rotate refresh + access cookies. */
106
+ /** POST /api/iqauth/refresh — rotate refresh + access cookies.
107
+ *
108
+ * Cookie-clearing policy is governed by `config.clearCookiesOnRefreshFailure`
109
+ * (default `"terminal-only"`). Prior to 2.0.3 this helper wiped both cookies
110
+ * on any non-2xx, which converted survivable refresh-token races (multi-tab,
111
+ * proactive-refresh timer, React StrictMode double-mount) into silent forced
112
+ * sign-outs. The default behavior now preserves cookies on transient failures
113
+ * and only clears them when the issuer signals the session is truly dead.
114
+ */
88
115
  declare function handleRefresh(config: IQAuthHelperConfig, input: {
89
116
  refreshToken?: string;
90
117
  }): Promise<HandlerResponse>;
@@ -63,14 +63,33 @@ interface IQAuthHelperConfig {
63
63
  logoutPath?: string;
64
64
  /** Optional fetch implementation override. */
65
65
  fetchImpl?: typeof fetch;
66
+ /**
67
+ * Policy for clearing the access + refresh cookies when `/refresh` fails.
68
+ *
69
+ * - `"terminal-only"` (default, recommended): only clear cookies when the
70
+ * issuer indicates the session is unrecoverable
71
+ * (`TOKEN_REVOKED`, `SESSION_REVOKED`, `INVALID_GRANT`,
72
+ * `USER_DEACTIVATED`, or HTTP 410 Gone). Transient failures
73
+ * (`TOKEN_INVALID` from a rotated-out token, `TOKEN_EXPIRED`, network
74
+ * errors, 5xx) leave cookies intact so the next legitimate request can
75
+ * either succeed against a still-valid access cookie or be redirected
76
+ * cleanly to sign-in by the middleware. Fixes the multi-tab /
77
+ * proactive-refresh race that previously silently signed users out.
78
+ * - `"always"`: pre-2.0.3 behavior — wipe both cookies on any non-2xx.
79
+ * Use only if you have an external reason to depend on the old semantics.
80
+ * - `"never"`: leave cookies untouched on every failure path. Suitable for
81
+ * apps that manage cookie lifecycle entirely outside the SDK helpers.
82
+ */
83
+ clearCookiesOnRefreshFailure?: "terminal-only" | "always" | "never";
66
84
  }
67
- interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl">> {
85
+ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure">> {
68
86
  secretKey?: string;
69
87
  cookieDomain?: string;
70
88
  issuer: string;
71
89
  fetchImpl: typeof fetch;
72
90
  appId: string;
73
91
  tenantId: string;
92
+ clearCookiesOnRefreshFailure: "terminal-only" | "always" | "never";
74
93
  }
75
94
  /**
76
95
  * Serialize a cookie directive to a Set-Cookie header value. Adapters that
@@ -84,7 +103,15 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
84
103
  codeVerifier?: string;
85
104
  redirectUri?: string;
86
105
  }): Promise<HandlerResponse>;
87
- /** POST /api/iqauth/refresh — rotate refresh + access cookies. */
106
+ /** POST /api/iqauth/refresh — rotate refresh + access cookies.
107
+ *
108
+ * Cookie-clearing policy is governed by `config.clearCookiesOnRefreshFailure`
109
+ * (default `"terminal-only"`). Prior to 2.0.3 this helper wiped both cookies
110
+ * on any non-2xx, which converted survivable refresh-token races (multi-tab,
111
+ * proactive-refresh timer, React StrictMode double-mount) into silent forced
112
+ * sign-outs. The default behavior now preserves cookies on transient failures
113
+ * and only clears them when the issuer signals the session is truly dead.
114
+ */
88
115
  declare function handleRefresh(config: IQAuthHelperConfig, input: {
89
116
  refreshToken?: string;
90
117
  }): Promise<HandlerResponse>;
@@ -57,6 +57,22 @@ function parsePublishableKey(raw) {
57
57
  }
58
58
 
59
59
  // src/server/handlers.ts
60
+ var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
61
+ "TOKEN_REVOKED",
62
+ "SESSION_REVOKED",
63
+ "INVALID_GRANT",
64
+ "invalid_grant",
65
+ "USER_DEACTIVATED",
66
+ "USER_DISABLED",
67
+ "TENANT_SUSPENDED"
68
+ ]);
69
+ function shouldClearCookiesOnFailure(policy, status, errorCode) {
70
+ if (policy === "always") return true;
71
+ if (policy === "never") return false;
72
+ if (status === 410) return true;
73
+ if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
74
+ return false;
75
+ }
60
76
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
61
77
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
62
78
  function resolve(config) {
@@ -84,7 +100,8 @@ function resolve(config) {
84
100
  throw new Error("global fetch is unavailable; pass fetchImpl");
85
101
  })),
86
102
  appId: parsed.appId,
87
- tenantId: parsed.tenantId
103
+ tenantId: parsed.tenantId,
104
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
88
105
  };
89
106
  }
90
107
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -180,7 +197,7 @@ async function handleRefresh(config, input) {
180
197
  return {
181
198
  status: 401,
182
199
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
183
- cookies: clearCookies(cfg)
200
+ cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
184
201
  };
185
202
  }
186
203
  const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
@@ -190,16 +207,23 @@ async function handleRefresh(config, input) {
190
207
  });
191
208
  const json = await res.json().catch(() => ({}));
192
209
  if (!res.ok || !json.success || !json.data?.accessToken) {
210
+ const status = res.status || 401;
211
+ const errorCode = json.error?.code || "TOKEN_INVALID";
212
+ const shouldClear = shouldClearCookiesOnFailure(
213
+ cfg.clearCookiesOnRefreshFailure,
214
+ status,
215
+ errorCode
216
+ );
193
217
  return {
194
- status: res.status || 401,
218
+ status,
195
219
  body: {
196
220
  success: false,
197
221
  error: {
198
- code: json.error?.code || "TOKEN_INVALID",
222
+ code: errorCode,
199
223
  message: json.error?.message || "Refresh failed"
200
224
  }
201
225
  },
202
- cookies: clearCookies(cfg)
226
+ cookies: shouldClear ? clearCookies(cfg) : []
203
227
  };
204
228
  }
205
229
  const cookies = [
@@ -3,7 +3,7 @@ import {
3
3
  handleRefresh,
4
4
  handleSignout,
5
5
  serializeCookie
6
- } from "../chunk-5HF3OBNO.mjs";
6
+ } from "../chunk-JQRTY5MY.mjs";
7
7
  import "../chunk-5WFR6Y33.mjs";
8
8
  import "../chunk-Y6FXYEAI.mjs";
9
9
  export {
package/dist/server.js CHANGED
@@ -1988,6 +1988,22 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
1988
1988
  }
1989
1989
 
1990
1990
  // src/server/handlers.ts
1991
+ var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
1992
+ "TOKEN_REVOKED",
1993
+ "SESSION_REVOKED",
1994
+ "INVALID_GRANT",
1995
+ "invalid_grant",
1996
+ "USER_DEACTIVATED",
1997
+ "USER_DISABLED",
1998
+ "TENANT_SUSPENDED"
1999
+ ]);
2000
+ function shouldClearCookiesOnFailure(policy, status, errorCode) {
2001
+ if (policy === "always") return true;
2002
+ if (policy === "never") return false;
2003
+ if (status === 410) return true;
2004
+ if (errorCode && TERMINAL_REFRESH_ERROR_CODES.has(errorCode)) return true;
2005
+ return false;
2006
+ }
1991
2007
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
1992
2008
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
1993
2009
  function resolve(config) {
@@ -2015,7 +2031,8 @@ function resolve(config) {
2015
2031
  throw new Error("global fetch is unavailable; pass fetchImpl");
2016
2032
  })),
2017
2033
  appId: parsed.appId,
2018
- tenantId: parsed.tenantId
2034
+ tenantId: parsed.tenantId,
2035
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
2019
2036
  };
2020
2037
  }
2021
2038
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -2111,7 +2128,7 @@ async function handleRefresh(config, input) {
2111
2128
  return {
2112
2129
  status: 401,
2113
2130
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
2114
- cookies: clearCookies(cfg)
2131
+ cookies: cfg.clearCookiesOnRefreshFailure === "always" ? clearCookies(cfg) : []
2115
2132
  };
2116
2133
  }
2117
2134
  const res = await cfg.fetchImpl(`${cfg.issuer}${cfg.refreshPath}`, {
@@ -2121,16 +2138,23 @@ async function handleRefresh(config, input) {
2121
2138
  });
2122
2139
  const json = await res.json().catch(() => ({}));
2123
2140
  if (!res.ok || !json.success || !json.data?.accessToken) {
2141
+ const status = res.status || 401;
2142
+ const errorCode = json.error?.code || "TOKEN_INVALID";
2143
+ const shouldClear = shouldClearCookiesOnFailure(
2144
+ cfg.clearCookiesOnRefreshFailure,
2145
+ status,
2146
+ errorCode
2147
+ );
2124
2148
  return {
2125
- status: res.status || 401,
2149
+ status,
2126
2150
  body: {
2127
2151
  success: false,
2128
2152
  error: {
2129
- code: json.error?.code || "TOKEN_INVALID",
2153
+ code: errorCode,
2130
2154
  message: json.error?.message || "Refresh failed"
2131
2155
  }
2132
2156
  },
2133
- cookies: clearCookies(cfg)
2157
+ cookies: shouldClear ? clearCookies(cfg) : []
2134
2158
  };
2135
2159
  }
2136
2160
  const cookies = [
package/dist/server.mjs CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  handleRefresh,
9
9
  handleSignout,
10
10
  serializeCookie
11
- } from "./chunk-5HF3OBNO.mjs";
11
+ } from "./chunk-JQRTY5MY.mjs";
12
12
  import "./chunk-5WFR6Y33.mjs";
13
13
  import {
14
14
  IQAuthClient
@@ -80,6 +80,27 @@ interface SessionManagerOptions {
80
80
  * refresh to finish before falling back to its own. Defaults to 4000.
81
81
  */
82
82
  crossTabLockTimeoutMs?: number;
83
+ /**
84
+ * "Server-managed" session mode for apps where the backend (via the
85
+ * `@iqauth/sdk/{express,fastify,hono,next}` adapters) owns the HttpOnly
86
+ * `iqauth_at` + `iqauth_rt` cookies and is the sole authority on token
87
+ * rotation. When `true`:
88
+ *
89
+ * - `bootstrap()` learns session state with a single `GET userinfoPath`
90
+ * call (no token rotation, no race surface), instead of POSTing to
91
+ * `/refresh` with an empty body.
92
+ * - `scheduleProactiveRefresh()` is suppressed — the server middleware
93
+ * refreshes on real navigation, which is single-flight per request.
94
+ * - `tokenStore` defaults to a no-op store so JS never tries to read the
95
+ * (HttpOnly, invisible) refresh cookie.
96
+ *
97
+ * This is the recommended mode for any app with its own backend, especially
98
+ * confidential-client OIDC flows. It eliminates the multi-tab refresh-token
99
+ * rotation race that previously caused silent sign-outs.
100
+ *
101
+ * Defaults to `false` to preserve pre-2.0.3 behavior.
102
+ */
103
+ serverManagedSession?: boolean;
83
104
  }
84
105
  declare class SessionManager {
85
106
  private snapshot;
@@ -96,6 +117,7 @@ declare class SessionManager {
96
117
  private readonly proactiveRefresh;
97
118
  private readonly tokenStore;
98
119
  private readonly crossTabLockTimeoutMs;
120
+ private readonly serverManagedSession;
99
121
  private proactiveTimer;
100
122
  private bootstrapped;
101
123
  /** Pending refresh awaited by other tabs after a `refresh:claim` from us. */
@@ -80,6 +80,27 @@ interface SessionManagerOptions {
80
80
  * refresh to finish before falling back to its own. Defaults to 4000.
81
81
  */
82
82
  crossTabLockTimeoutMs?: number;
83
+ /**
84
+ * "Server-managed" session mode for apps where the backend (via the
85
+ * `@iqauth/sdk/{express,fastify,hono,next}` adapters) owns the HttpOnly
86
+ * `iqauth_at` + `iqauth_rt` cookies and is the sole authority on token
87
+ * rotation. When `true`:
88
+ *
89
+ * - `bootstrap()` learns session state with a single `GET userinfoPath`
90
+ * call (no token rotation, no race surface), instead of POSTing to
91
+ * `/refresh` with an empty body.
92
+ * - `scheduleProactiveRefresh()` is suppressed — the server middleware
93
+ * refreshes on real navigation, which is single-flight per request.
94
+ * - `tokenStore` defaults to a no-op store so JS never tries to read the
95
+ * (HttpOnly, invisible) refresh cookie.
96
+ *
97
+ * This is the recommended mode for any app with its own backend, especially
98
+ * confidential-client OIDC flows. It eliminates the multi-tab refresh-token
99
+ * rotation race that previously caused silent sign-outs.
100
+ *
101
+ * Defaults to `false` to preserve pre-2.0.3 behavior.
102
+ */
103
+ serverManagedSession?: boolean;
83
104
  }
84
105
  declare class SessionManager {
85
106
  private snapshot;
@@ -96,6 +117,7 @@ declare class SessionManager {
96
117
  private readonly proactiveRefresh;
97
118
  private readonly tokenStore;
98
119
  private readonly crossTabLockTimeoutMs;
120
+ private readonly serverManagedSession;
99
121
  private proactiveTimer;
100
122
  private bootstrapped;
101
123
  /** Pending refresh awaited by other tabs after a `refresh:claim` from us. */
@@ -2,6 +2,40 @@
2
2
 
3
3
  Use this guide when a first-party web app currently owns IQAuth tokens in browser code and needs to move to the supported session model.
4
4
 
5
+ ## Cookie-managed / confidential-client (recommended for any app with a backend)
6
+
7
+ If your app uses one of the framework adapters (`@iqauth/sdk/{express,fastify,hono,next}`) the backend owns the HttpOnly `iqauth_at` + `iqauth_rt` cookies and should be the **sole** authority on token rotation. In that setup the browser `SessionManager` must NOT also try to rotate refresh tokens — otherwise the proactive-refresh timer + multi-tab BroadcastChannel + React StrictMode double-mount can race against the backend's own refreshes, and one lost race used to wipe the cookies.
8
+
9
+ As of `@iqauth/sdk@2.0.3`, opt into "server-managed" mode with a single flag:
10
+
11
+ ```ts
12
+ import { SessionManager } from "@iqauth/sdk/browser";
13
+
14
+ const manager = new SessionManager({
15
+ publishableKey: process.env.NEXT_PUBLIC_IQAUTH_PUBLISHABLE_KEY!,
16
+ issuer: window.location.origin,
17
+ refreshPath: "/api/iqauth/refresh", // mounted by the framework adapter
18
+ serverManagedSession: true, // ← do this
19
+ });
20
+ ```
21
+
22
+ What `serverManagedSession: true` does:
23
+
24
+ - `bootstrap()` learns session state with a single read-only `GET /api/v1/auth/me` (override with `userinfoPath`) instead of POSTing to `/refresh`. No rotation, no race surface.
25
+ - `scheduleProactiveRefresh()` is suppressed. The server middleware refreshes on real navigation, which is single-flight per request and can't race itself.
26
+ - `tokenStore` defaults to a no-op store, so JS never tries to read the (HttpOnly, invisible) refresh cookie.
27
+
28
+ `<SignedIn>` / `<SignedOut>` / `useUser()` continue to work — they read from the userinfo bootstrap.
29
+
30
+ ### Cookie-clearing policy on `/refresh` failures
31
+
32
+ `@iqauth/sdk/server::handleRefresh` now defaults to `clearCookiesOnRefreshFailure: "terminal-only"`. Cookies are only wiped when the issuer signals the session is unrecoverable (`TOKEN_REVOKED`, `SESSION_REVOKED`, `INVALID_GRANT`, `USER_DEACTIVATED`, `TENANT_SUSPENDED`, or HTTP 410 Gone). Transient failures — `TOKEN_INVALID` from a rotated-out token, `TOKEN_EXPIRED`, network blips, 5xx — return 401 without touching cookies, so the next legitimate request can either succeed against a still-valid access cookie or get redirected to sign-in cleanly by the middleware.
33
+
34
+ Pre-2.0.3 behavior is available with `clearCookiesOnRefreshFailure: "always"`.
35
+
36
+ ---
37
+
38
+
5
39
  ## Target Model
6
40
 
7
41
  - browser talks to app backend
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iqauth/sdk",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "TypeScript SDK for IQAuth — the canonical way for all IQ projects to integrate with IQAuthService",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",