@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
@@ -3,15 +3,16 @@ import {
3
3
  clearCookie,
4
4
  getCookie,
5
5
  setCookie
6
- } from "./chunk-TKZTCPEK.mjs";
6
+ } from "./chunk-GN37E64I.mjs";
7
7
  import {
8
8
  assertPublishableKey
9
- } from "./chunk-WQWBJSSS.mjs";
9
+ } from "./chunk-HVHNYPDC.mjs";
10
10
  import {
11
11
  IQAuthError
12
- } from "./chunk-6I6RM4MN.mjs";
12
+ } from "./chunk-6PJRLRB4.mjs";
13
13
 
14
14
  // src/browser/sessionManager.ts
15
+ var PROBE_WAIT_MS = 80;
15
16
  var DEFAULT_REFRESH_PATH = "/api/v1/auth/refresh";
16
17
  var DEFAULT_USERINFO_PATH = "/api/v1/auth/me";
17
18
  async function readAuthErrorCode(res) {
@@ -49,7 +50,13 @@ function claimsToSessionUser(claims) {
49
50
  tenantId: claims.tenantId,
50
51
  vendorId: claims.vendorId,
51
52
  roles: claims.roles ?? [],
52
- entitlements: claims.entitlements ?? []
53
+ entitlements: claims.entitlements ?? [],
54
+ // SDK 2.7.0 (Task #124) — pass through identity claims when issued.
55
+ ...claims.picture !== void 0 ? { picture: claims.picture } : {},
56
+ ...claims.email_verified !== void 0 ? { emailVerified: claims.email_verified } : {},
57
+ ...claims.given_name !== void 0 ? { givenName: claims.given_name } : {},
58
+ ...claims.family_name !== void 0 ? { familyName: claims.family_name } : {},
59
+ ...claims.locale !== void 0 ? { locale: claims.locale } : {}
53
60
  };
54
61
  }
55
62
  var EMPTY = {
@@ -73,11 +80,50 @@ var NO_OP_STORE = {
73
80
  write: () => void 0,
74
81
  clear: () => void 0
75
82
  };
83
+ var IDEMPOTENCY_HEADER = "X-IQAuth-Idempotency";
84
+ function randomIdempotencyToken() {
85
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
86
+ const bytes = new Uint8Array(16);
87
+ crypto.getRandomValues(bytes);
88
+ let out = "";
89
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
90
+ return out;
91
+ }
92
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
93
+ }
76
94
  var SessionManager = class {
77
95
  constructor(options) {
78
96
  this.snapshot = { ...EMPTY };
79
97
  this.listeners = /* @__PURE__ */ new Set();
80
98
  this.refreshPromise = null;
99
+ /**
100
+ * Cancellation handle for the in-flight refresh, if any. `signOut()` (or a
101
+ * `session:signout` broadcast from another tab) calls `abort()` so the
102
+ * refresh response is dropped before it can write a fresh access cookie
103
+ * on top of the just-cleared session — the second root cause of "ghost
104
+ * signed-in" sessions after Sign Out.
105
+ */
106
+ this.refreshAbort = null;
107
+ /**
108
+ * Set to `true` by `signOut()` / `signOutLocal()` for the lifetime of the
109
+ * call. Used as a safety belt: even if a refresh response arrives while
110
+ * `refreshAbort` was unable to interrupt the network call (e.g. the body
111
+ * was already streaming back), `runRefresh` checks this flag before
112
+ * mutating session state and bails out.
113
+ */
114
+ this.signoutInProgress = false;
115
+ /**
116
+ * Per-session opaque idempotency token. Sent as `X-IQAuth-Idempotency` on
117
+ * every /refresh and /signout request the SDK makes through a framework
118
+ * adapter (Express/Fastify/Hono/Next), so the adapter's `SignoutRegistry`
119
+ * can collapse a refresh that lands moments after a signout — even when
120
+ * the two requests are routed to different server instances (multi-replica
121
+ * deployments).
122
+ *
123
+ * Generated lazily on first use, rotated on signout so the next session
124
+ * starts with a fresh token. Opaque random — never the raw refresh token.
125
+ */
126
+ this.idempotencyToken = null;
81
127
  this.channel = null;
82
128
  this.proactiveTimer = null;
83
129
  this.bootstrapped = false;
@@ -85,6 +131,8 @@ var SessionManager = class {
85
131
  this.remoteRefreshWaiters = [];
86
132
  /** Active claims by other tabs (keyed by source tabId). */
87
133
  this.foreignClaim = null;
134
+ /** Resolver for an in-flight cross-tab `session:probe`, set during bootstrap. */
135
+ this.probeResolver = null;
88
136
  const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
89
137
  this.key = parsed;
90
138
  const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
@@ -97,6 +145,8 @@ var SessionManager = class {
97
145
  this.refreshCookieName = options.cookieNames?.refresh ?? REFRESH_COOKIE;
98
146
  this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore(this.refreshCookieName) : NO_OP_STORE);
99
147
  this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
148
+ this.debug = options.debug ?? false;
149
+ this.onTimingEvent = options.onTimingEvent ?? null;
100
150
  this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
101
151
  throw new Error("global fetch is not available; pass fetchImpl");
102
152
  }));
@@ -123,10 +173,35 @@ var SessionManager = class {
123
173
  get issuerUrl() {
124
174
  return this.issuer;
125
175
  }
176
+ /**
177
+ * SDK 2.7.0 (Task #124) — The hosted IQAuth host derived from the
178
+ * publishable key's `iss` claim, normalized to URL form. This is what
179
+ * `<SignIn/>` and `buildSignInUrl` use to talk to the hosted UI; it
180
+ * deliberately ignores the `issuer` constructor override so a misrouted
181
+ * `issuer` (e.g. pointed at the consumer app's own domain) cannot break
182
+ * the hosted flow. Use {@link issuerUrl} for token / discovery endpoints.
183
+ */
184
+ get hostedIssuerUrl() {
185
+ const iss = this.key.iss;
186
+ return (iss.startsWith("http") ? iss : `https://${iss}`).replace(/\/+$/, "");
187
+ }
126
188
  /** Cookie name the SDK uses for the refresh token (overridable via `cookieNames.refresh`). */
127
189
  get refreshCookie() {
128
190
  return this.refreshCookieName;
129
191
  }
192
+ /**
193
+ * Returns the current per-session idempotency token, generating one
194
+ * lazily on first use. Sent as the `X-IQAuth-Idempotency` header on
195
+ * /refresh and /signout requests so the framework adapter's
196
+ * `SignoutRegistry` can collapse a refresh-vs-signout race even across
197
+ * server instances.
198
+ */
199
+ getIdempotencyToken() {
200
+ if (!this.idempotencyToken) {
201
+ this.idempotencyToken = randomIdempotencyToken();
202
+ }
203
+ return this.idempotencyToken;
204
+ }
130
205
  getSnapshot() {
131
206
  return this.snapshot;
132
207
  }
@@ -140,9 +215,44 @@ var SessionManager = class {
140
215
  * One-time bootstrap: warm the session from the refresh cookie if present.
141
216
  * Safe to call multiple times.
142
217
  */
218
+ /**
219
+ * Task #126: Public timing-event emitter. Used by the browser sign-in
220
+ * helpers (redirectToSignIn / handleAuthCallback) to surface signIn-phase
221
+ * timings through the same `debug` + `onTimingEvent` channel as
222
+ * bootstrap/refresh. Safe to call from anywhere — internal callers
223
+ * pre-compute durationMs.
224
+ */
225
+ recordTiming(phase, durationMs, ok, code) {
226
+ this.emitTiming(phase, durationMs, ok, code);
227
+ }
228
+ /** Task #126: emit a session timing event to debug log + onTimingEvent hook. */
229
+ emitTiming(phase, durationMs, ok, code) {
230
+ if (this.debug) {
231
+ try {
232
+ console.debug("[iqauth_session]", { phase, durationMs, ok, code });
233
+ } catch {
234
+ }
235
+ }
236
+ if (this.onTimingEvent) {
237
+ try {
238
+ this.onTimingEvent({ phase, durationMs, ok, code });
239
+ } catch {
240
+ }
241
+ }
242
+ }
143
243
  async bootstrap() {
144
244
  if (this.bootstrapped) return;
145
245
  this.bootstrapped = true;
246
+ const t0 = Date.now();
247
+ try {
248
+ await this.bootstrapInner();
249
+ this.emitTiming("bootstrap", Date.now() - t0, this.snapshot.status === "authenticated");
250
+ } catch (err) {
251
+ this.emitTiming("bootstrap", Date.now() - t0, false, err instanceof Error ? err.message : "ERROR");
252
+ throw err;
253
+ }
254
+ }
255
+ async bootstrapInner() {
146
256
  if (this.serverManagedSession) {
147
257
  try {
148
258
  const res = await this.fetchImpl(`${this.issuer}${this.userinfoPath}`, {
@@ -177,6 +287,15 @@ var SessionManager = class {
177
287
  return;
178
288
  }
179
289
  }
290
+ const peerSnapshot = await this.probePeers();
291
+ if (peerSnapshot && peerSnapshot.status === "authenticated") {
292
+ this.update({
293
+ ...peerSnapshot,
294
+ version: Math.max(this.snapshot.version, peerSnapshot.version) + 1
295
+ });
296
+ this.scheduleProactiveRefresh();
297
+ return;
298
+ }
180
299
  const stored = await Promise.resolve(this.tokenStore.read());
181
300
  if (!stored) {
182
301
  this.setStatus("unauthenticated");
@@ -185,6 +304,22 @@ var SessionManager = class {
185
304
  const ok = await this.refresh();
186
305
  if (!ok) this.setStatus("unauthenticated");
187
306
  }
307
+ probePeers() {
308
+ if (!this.channel) return Promise.resolve(null);
309
+ return new Promise((resolve) => {
310
+ let settled = false;
311
+ const finish = (snap) => {
312
+ if (settled) return;
313
+ settled = true;
314
+ this.probeResolver = null;
315
+ clearTimeout(timer);
316
+ resolve(snap);
317
+ };
318
+ this.probeResolver = (snap) => finish(snap);
319
+ const timer = setTimeout(() => finish(null), PROBE_WAIT_MS);
320
+ this.broadcastEnvelope({ type: "session:probe", source: this.tabId, ts: Date.now() });
321
+ });
322
+ }
188
323
  /**
189
324
  * Single-flight token refresh, coordinated across tabs via BroadcastChannel.
190
325
  *
@@ -198,30 +333,48 @@ var SessionManager = class {
198
333
  */
199
334
  refresh() {
200
335
  if (this.refreshPromise) return this.refreshPromise;
201
- this.refreshPromise = this.runRefresh().finally(() => {
336
+ const t0 = Date.now();
337
+ this.refreshPromise = this.runRefresh().then((ok) => {
338
+ this.emitTiming("refresh", Date.now() - t0, ok, ok ? void 0 : this.snapshot.error?.code ?? "REFRESH_FAILED");
339
+ return ok;
340
+ }).finally(() => {
202
341
  this.refreshPromise = null;
203
342
  });
204
343
  return this.refreshPromise;
205
344
  }
206
345
  async runRefresh() {
207
- const myClaim = { source: this.tabId, ts: Date.now() };
208
- if (this.channel) {
209
- this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
210
- await new Promise((r) => setTimeout(r, 25));
211
- const foreign = this.foreignClaim;
212
- if (foreign && this.claimWins(foreign, myClaim)) {
213
- return this.waitForForeignRefresh();
214
- }
215
- }
346
+ const abort = new AbortController();
347
+ this.refreshAbort = abort;
216
348
  try {
349
+ const myClaim = { source: this.tabId, ts: Date.now() };
350
+ if (this.channel) {
351
+ this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
352
+ await new Promise((r) => setTimeout(r, 25));
353
+ if (abort.signal.aborted || this.signoutInProgress) {
354
+ this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
355
+ return false;
356
+ }
357
+ const foreign = this.foreignClaim;
358
+ if (foreign && this.claimWins(foreign, myClaim)) {
359
+ return this.waitForForeignRefresh();
360
+ }
361
+ }
217
362
  const refreshToken = await Promise.resolve(this.tokenStore.read());
218
363
  const res = await this.fetchImpl(`${this.issuer}${this.refreshPath}`, {
219
364
  method: "POST",
220
365
  credentials: "include",
221
- headers: { "Content-Type": "application/json" },
222
- body: JSON.stringify(refreshToken ? { refreshToken } : {})
366
+ headers: {
367
+ "Content-Type": "application/json",
368
+ [IDEMPOTENCY_HEADER]: this.getIdempotencyToken()
369
+ },
370
+ body: JSON.stringify(refreshToken ? { refreshToken } : {}),
371
+ signal: abort.signal
223
372
  });
224
373
  const body = await res.json().catch(() => ({}));
374
+ if (this.signoutInProgress || abort.signal.aborted) {
375
+ this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
376
+ return false;
377
+ }
225
378
  const data = body.data;
226
379
  if (!res.ok || !body.success || !data?.accessToken) {
227
380
  const err = body.error;
@@ -240,14 +393,18 @@ var SessionManager = class {
240
393
  this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: true });
241
394
  return true;
242
395
  } catch (err) {
243
- this.setError({
244
- code: "NETWORK_ERROR",
245
- message: err instanceof Error ? err.message : "Refresh request failed"
246
- });
396
+ const aborted = err?.name === "AbortError" || abort.signal.aborted || this.signoutInProgress;
397
+ if (!aborted) {
398
+ this.setError({
399
+ code: "NETWORK_ERROR",
400
+ message: err instanceof Error ? err.message : "Refresh request failed"
401
+ });
402
+ }
247
403
  this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
248
404
  return false;
249
405
  } finally {
250
406
  this.foreignClaim = null;
407
+ if (this.refreshAbort === abort) this.refreshAbort = null;
251
408
  }
252
409
  }
253
410
  claimWins(foreign, mine) {
@@ -355,6 +512,14 @@ var SessionManager = class {
355
512
  * the server-side logout request.
356
513
  */
357
514
  signOutLocal(status = "unauthenticated") {
515
+ this.signoutInProgress = true;
516
+ if (this.refreshAbort) {
517
+ try {
518
+ this.refreshAbort.abort();
519
+ } catch {
520
+ }
521
+ this.refreshAbort = null;
522
+ }
358
523
  void Promise.resolve(this.tokenStore.clear());
359
524
  if (this.proactiveTimer) {
360
525
  clearTimeout(this.proactiveTimer);
@@ -369,7 +534,12 @@ var SessionManager = class {
369
534
  error: null,
370
535
  version: this.snapshot.version + 1
371
536
  });
537
+ this.broadcastEnvelope({ type: "refresh:abort", source: this.tabId, ts: Date.now() });
372
538
  this.broadcast("session:signout");
539
+ this.idempotencyToken = null;
540
+ setTimeout(() => {
541
+ this.signoutInProgress = false;
542
+ }, 0);
373
543
  }
374
544
  /**
375
545
  * Replace the refresh-token store at runtime. Used by the F22
@@ -433,6 +603,12 @@ var SessionManager = class {
433
603
  }
434
604
  onBroadcast(env) {
435
605
  if (!env || env.source === this.tabId) return;
606
+ if (env.type === "session:probe") {
607
+ if (this.snapshot.status === "authenticated") {
608
+ this.broadcast("session:update");
609
+ }
610
+ return;
611
+ }
436
612
  if (env.type === "refresh:claim") {
437
613
  this.foreignClaim = { source: env.source, ts: env.ts };
438
614
  return;
@@ -445,6 +621,24 @@ var SessionManager = class {
445
621
  this.foreignClaim = null;
446
622
  return;
447
623
  }
624
+ if (env.type === "refresh:abort") {
625
+ this.signoutInProgress = true;
626
+ if (this.refreshAbort) {
627
+ try {
628
+ this.refreshAbort.abort();
629
+ } catch {
630
+ }
631
+ this.refreshAbort = null;
632
+ }
633
+ const waiters = this.remoteRefreshWaiters;
634
+ this.remoteRefreshWaiters = [];
635
+ for (const w of waiters) w(false);
636
+ this.foreignClaim = null;
637
+ setTimeout(() => {
638
+ this.signoutInProgress = false;
639
+ }, 0);
640
+ return;
641
+ }
448
642
  if (env.type === "session:signout") {
449
643
  this.update({
450
644
  status: "unauthenticated",
@@ -458,6 +652,12 @@ var SessionManager = class {
458
652
  return;
459
653
  }
460
654
  if ((env.type === "session:update" || env.type === "session:refresh") && env.payload) {
655
+ if (this.probeResolver && env.payload.status === "authenticated") {
656
+ const r = this.probeResolver;
657
+ this.probeResolver = null;
658
+ r(env.payload);
659
+ return;
660
+ }
461
661
  this.update({
462
662
  ...env.payload,
463
663
  version: Math.max(this.snapshot.version, env.payload.version) + 1
@@ -1,8 +1,49 @@
1
1
  import {
2
2
  assertPublishableKey
3
- } from "./chunk-WQWBJSSS.mjs";
3
+ } from "./chunk-HVHNYPDC.mjs";
4
+ import {
5
+ TokensModule
6
+ } from "./chunk-NUO2I65G.mjs";
7
+ import {
8
+ IQAuthError
9
+ } from "./chunk-6PJRLRB4.mjs";
4
10
 
5
11
  // src/server/handlers.ts
12
+ async function buildUserinfoResponse(claims, opts = {}) {
13
+ const baseUser = {
14
+ sub: claims.sub,
15
+ email: claims.email,
16
+ name: claims.name,
17
+ tenantId: claims.tenantId,
18
+ vendorId: claims.vendorId,
19
+ roles: claims.roles ?? [],
20
+ entitlements: claims.entitlements ?? []
21
+ };
22
+ const enriched = opts.enrich ? await opts.enrich(claims) : null;
23
+ const user = enriched ? { ...baseUser, ...enriched } : baseUser;
24
+ return {
25
+ success: true,
26
+ data: {
27
+ user,
28
+ claims,
29
+ tenantId: claims.tenantId ?? null
30
+ }
31
+ };
32
+ }
33
+ function emitTiming(cfg, event) {
34
+ if (cfg.debug) {
35
+ try {
36
+ console.debug("[iqauth_helper]", event);
37
+ } catch {
38
+ }
39
+ }
40
+ if (cfg.onTimingEvent) {
41
+ try {
42
+ cfg.onTimingEvent(event);
43
+ } catch {
44
+ }
45
+ }
46
+ }
6
47
  var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
7
48
  "TOKEN_REVOKED",
8
49
  "SESSION_REVOKED",
@@ -42,7 +83,11 @@ function resolve(config) {
42
83
  })),
43
84
  appId: parsed.appId,
44
85
  tenantId: parsed.tenantId,
45
- clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
86
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
87
+ debug: config.debug,
88
+ onTimingEvent: config.onTimingEvent,
89
+ signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
90
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
46
91
  };
47
92
  }
48
93
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -59,15 +104,64 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
59
104
  }
60
105
  function clearCookies(cfg) {
61
106
  return [
62
- makeCookie(cfg, cfg.accessCookieName, "", 0),
63
- makeCookie(cfg, cfg.refreshCookieName, "", 0)
107
+ { ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
108
+ { ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
64
109
  ];
65
110
  }
111
+ var DEFAULT_SIGNOUT_TTL_MS = 6e4;
112
+ var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
113
+ function pruneInMemoryMarkers(now) {
114
+ if (inMemorySignoutMarkers.size === 0) return;
115
+ for (const [k, exp] of inMemorySignoutMarkers) {
116
+ if (exp <= now) inMemorySignoutMarkers.delete(k);
117
+ }
118
+ }
119
+ var defaultSignoutRegistry = {
120
+ mark(token, ttlMs) {
121
+ const now = Date.now();
122
+ pruneInMemoryMarkers(now);
123
+ inMemorySignoutMarkers.set(token, now + ttlMs);
124
+ },
125
+ has(token) {
126
+ const now = Date.now();
127
+ const exp = inMemorySignoutMarkers.get(token);
128
+ if (!exp) return false;
129
+ if (exp <= now) {
130
+ inMemorySignoutMarkers.delete(token);
131
+ return false;
132
+ }
133
+ return true;
134
+ }
135
+ };
136
+ function __resetSignoutMarkersForTests() {
137
+ inMemorySignoutMarkers.clear();
138
+ }
139
+ function createInMemorySignoutRegistry() {
140
+ const store = /* @__PURE__ */ new Map();
141
+ return {
142
+ mark(token, ttlMs) {
143
+ const now = Date.now();
144
+ for (const [k, exp] of store) if (exp <= now) store.delete(k);
145
+ store.set(token, now + ttlMs);
146
+ },
147
+ has(token) {
148
+ const now = Date.now();
149
+ const exp = store.get(token);
150
+ if (!exp) return false;
151
+ if (exp <= now) {
152
+ store.delete(token);
153
+ return false;
154
+ }
155
+ return true;
156
+ }
157
+ };
158
+ }
66
159
  function serializeCookie(d) {
67
160
  const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
68
161
  parts.push(`Path=${d.path}`);
69
162
  if (d.domain) parts.push(`Domain=${d.domain}`);
70
163
  parts.push(`Max-Age=${d.maxAge}`);
164
+ if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
71
165
  if (d.secure) parts.push("Secure");
72
166
  if (d.httpOnly) parts.push("HttpOnly");
73
167
  parts.push(`SameSite=${d.sameSite}`);
@@ -75,7 +169,9 @@ function serializeCookie(d) {
75
169
  }
76
170
  async function handleCallback(config, input) {
77
171
  const cfg = resolve(config);
172
+ const t0 = Date.now();
78
173
  if (!input.code || !input.redirectUri) {
174
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
79
175
  return {
80
176
  status: 400,
81
177
  body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
@@ -83,6 +179,7 @@ async function handleCallback(config, input) {
83
179
  };
84
180
  }
85
181
  if (!cfg.secretKey) {
182
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
86
183
  return {
87
184
  status: 500,
88
185
  body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
@@ -106,6 +203,7 @@ async function handleCallback(config, input) {
106
203
  });
107
204
  const json = await res.json().catch(() => ({}));
108
205
  if (!res.ok || !json.access_token) {
206
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
109
207
  return {
110
208
  status: res.status || 502,
111
209
  body: {
@@ -125,6 +223,7 @@ async function handleCallback(config, input) {
125
223
  if (json.refresh_token) {
126
224
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
127
225
  }
226
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
128
227
  return {
129
228
  status: 200,
130
229
  body: { success: true, data: { authenticated: true } },
@@ -133,8 +232,18 @@ async function handleCallback(config, input) {
133
232
  }
134
233
  async function handleRefresh(config, input) {
135
234
  const cfg = resolve(config);
235
+ const t0 = Date.now();
136
236
  const refreshToken = input.refreshToken;
237
+ const idemKey = input.idempotencyToken;
238
+ if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
239
+ return {
240
+ status: 401,
241
+ body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
242
+ cookies: clearCookies(cfg)
243
+ };
244
+ }
137
245
  if (!refreshToken) {
246
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
138
247
  return {
139
248
  status: 401,
140
249
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
@@ -150,6 +259,7 @@ async function handleRefresh(config, input) {
150
259
  if (!res.ok || !json.success || !json.data?.accessToken) {
151
260
  const status = res.status || 401;
152
261
  const errorCode = json.error?.code || "TOKEN_INVALID";
262
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
153
263
  const shouldClear = shouldClearCookiesOnFailure(
154
264
  cfg.clearCookiesOnRefreshFailure,
155
265
  status,
@@ -173,6 +283,7 @@ async function handleRefresh(config, input) {
173
283
  if (json.data.refreshToken) {
174
284
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
175
285
  }
286
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
176
287
  return {
177
288
  status: 200,
178
289
  body: { success: true, data: { accessToken: json.data.accessToken } },
@@ -181,6 +292,10 @@ async function handleRefresh(config, input) {
181
292
  }
182
293
  async function handleSignout(config, input) {
183
294
  const cfg = resolve(config);
295
+ const t0 = Date.now();
296
+ if (input.idempotencyToken) {
297
+ await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
298
+ }
184
299
  if (input.accessToken) {
185
300
  try {
186
301
  await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
@@ -202,16 +317,60 @@ async function handleSignout(config, input) {
202
317
  } catch {
203
318
  }
204
319
  }
320
+ emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
205
321
  return {
206
322
  status: 200,
207
323
  body: { success: true, data: { signedOut: true } },
208
324
  cookies: clearCookies(cfg)
209
325
  };
210
326
  }
327
+ var TOKENS_CACHE = /* @__PURE__ */ new Map();
328
+ function getTokensFor(issuer) {
329
+ let m = TOKENS_CACHE.get(issuer);
330
+ if (!m) {
331
+ m = new TokensModule(issuer);
332
+ TOKENS_CACHE.set(issuer, m);
333
+ }
334
+ return m;
335
+ }
336
+ async function handleUserinfo(config, input) {
337
+ const cfg = resolve(config);
338
+ if (!input.accessToken) {
339
+ return {
340
+ status: 401,
341
+ body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
342
+ cookies: []
343
+ };
344
+ }
345
+ let claims;
346
+ try {
347
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
348
+ } catch (err) {
349
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
350
+ const message = err instanceof Error ? err.message : "Access token verification failed";
351
+ return {
352
+ status: 401,
353
+ body: { success: false, error: { code, message } },
354
+ cookies: []
355
+ };
356
+ }
357
+ const envelope = await buildUserinfoResponse(claims, {
358
+ enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
359
+ });
360
+ return {
361
+ status: 200,
362
+ body: envelope,
363
+ cookies: []
364
+ };
365
+ }
211
366
 
212
367
  export {
368
+ buildUserinfoResponse,
369
+ __resetSignoutMarkersForTests,
370
+ createInMemorySignoutRegistry,
213
371
  serializeCookie,
214
372
  handleCallback,
215
373
  handleRefresh,
216
- handleSignout
374
+ handleSignout,
375
+ handleUserinfo
217
376
  };
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  assertPublishableKey
3
- } from "./chunk-WQWBJSSS.mjs";
3
+ } from "./chunk-HVHNYPDC.mjs";
4
4
  import {
5
5
  TokensModule
6
- } from "./chunk-UNYDG2L4.mjs";
6
+ } from "./chunk-NUO2I65G.mjs";
7
7
  import {
8
8
  IQAuthError
9
- } from "./chunk-6I6RM4MN.mjs";
9
+ } from "./chunk-6PJRLRB4.mjs";
10
10
 
11
11
  // src/ws.ts
12
12
  var DEFAULT_COOKIE = "iqauth_at";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  encodePublishableKey
3
- } from "./chunk-WQWBJSSS.mjs";
3
+ } from "./chunk-HVHNYPDC.mjs";
4
4
 
5
5
  // src/test.ts
6
6
  import { createServer } from "http";