@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
package/dist/next.js CHANGED
@@ -27,13 +27,30 @@ __export(next_exports, {
27
27
  module.exports = __toCommonJS(next_exports);
28
28
 
29
29
  // src/errors.ts
30
- var IQAuthError = class extends Error {
31
- constructor(code, message, status, raw) {
30
+ var IQAuthError = class _IQAuthError extends Error {
31
+ constructor(code, message, status, cause) {
32
32
  super(message);
33
33
  this.name = "IQAuthError";
34
34
  this.code = code;
35
35
  this.status = status;
36
- this.raw = raw;
36
+ this.cause = cause;
37
+ this.raw = cause;
38
+ }
39
+ /**
40
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
41
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
42
+ */
43
+ static isIQAuthError(value) {
44
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
45
+ }
46
+ /**
47
+ * Type-narrowed code check. Lets callers write
48
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
49
+ * taxonomy without losing the ability to handle server codes via
50
+ * `err.code === "TOKEN_REVOKED"`.
51
+ */
52
+ is(code) {
53
+ return this.code === code;
37
54
  }
38
55
  };
39
56
 
@@ -66,14 +83,14 @@ function assertPublishableKey(raw, opts) {
66
83
  const ctx = opts?.context ? `${opts.context}: ` : "";
67
84
  if (typeof raw !== "string" || raw.length === 0) {
68
85
  throw new IQAuthError(
69
- "CONFIG_INVALID",
86
+ "config_invalid",
70
87
  `${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.`
71
88
  );
72
89
  }
73
90
  const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
74
91
  if (!shapeMatch) {
75
92
  throw new IQAuthError(
76
- "CONFIG_INVALID",
93
+ "config_invalid",
77
94
  `${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.`
78
95
  );
79
96
  }
@@ -82,19 +99,19 @@ function assertPublishableKey(raw, opts) {
82
99
  decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
83
100
  } catch {
84
101
  throw new IQAuthError(
85
- "CONFIG_INVALID",
102
+ "config_invalid",
86
103
  `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
87
104
  );
88
105
  }
89
106
  if (!isPublishableKeyPayload(decoded)) {
90
107
  throw new IQAuthError(
91
- "CONFIG_INVALID",
108
+ "config_invalid",
92
109
  `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
93
110
  );
94
111
  }
95
112
  if (!isValidIssuerUrl(decoded.iss)) {
96
113
  throw new IQAuthError(
97
- "CONFIG_INVALID",
114
+ "config_invalid",
98
115
  `${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.`
99
116
  );
100
117
  }
@@ -106,7 +123,267 @@ function isPublishableKeyPayload(value) {
106
123
  return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
107
124
  }
108
125
 
126
+ // src/modules/tokens.ts
127
+ var import_jose = require("jose");
128
+ var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
129
+ var DEFAULT_TOKEN_ISSUER = [
130
+ "https://auth.dispositioniq.com",
131
+ "auth.dispositioniq.com"
132
+ ];
133
+ var DEFAULT_TOKEN_AUDIENCE = [
134
+ "dispositioniq",
135
+ "iqcapture",
136
+ "iqreuse",
137
+ "iqvalidate"
138
+ ];
139
+ var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
140
+ function classifyJoseError(err) {
141
+ if (err instanceof import_jose.errors.JWTExpired) {
142
+ return { code: "token_expired", message: "Token has expired" };
143
+ }
144
+ if (err instanceof import_jose.errors.JOSEError) {
145
+ return { code: "token_invalid", message: err.message };
146
+ }
147
+ if (err instanceof Error) {
148
+ return { code: "token_invalid", message: err.message };
149
+ }
150
+ return { code: "token_invalid", message: "Token verification failed" };
151
+ }
152
+ function decodeProtectedHeader(token) {
153
+ const parts = token.split(".");
154
+ if (parts.length < 2) return null;
155
+ try {
156
+ const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
157
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
158
+ let json;
159
+ if (typeof atob === "function") {
160
+ json = atob(b64);
161
+ } else {
162
+ const { Buffer: Buffer2 } = require("buffer");
163
+ json = Buffer2.from(b64, "base64").toString("utf8");
164
+ }
165
+ return JSON.parse(json);
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ var TokensModule = class {
171
+ constructor(baseUrl, options = {}) {
172
+ this.jwksCache = null;
173
+ this.inFlightRefresh = null;
174
+ this.baseUrl = baseUrl;
175
+ this.defaultIssuer = options.issuer ?? DEFAULT_TOKEN_ISSUER;
176
+ this.defaultAudience = options.audience ?? DEFAULT_TOKEN_AUDIENCE;
177
+ this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
178
+ }
179
+ /**
180
+ * Verify a JWT access token using RS256/ES256 via JWKS from
181
+ * `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
182
+ * Node, browser, and edge runtimes alike — no `node:crypto` dependency.
183
+ * Caches JWKS for 1 hour and refetches once on unknown `kid`.
184
+ */
185
+ async verify(token, options = {}) {
186
+ const header = decodeProtectedHeader(token);
187
+ if (!header) {
188
+ throw new IQAuthError("token_invalid", "Unable to decode token");
189
+ }
190
+ const kid = header.kid;
191
+ if (!kid) {
192
+ throw new IQAuthError("token_invalid", "Token missing kid header");
193
+ }
194
+ let cache = await this.ensureCache();
195
+ if (!cache.byKid.has(kid)) {
196
+ this.jwksCache = null;
197
+ cache = await this.ensureCache();
198
+ }
199
+ if (!cache.byKid.has(kid)) {
200
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
201
+ }
202
+ const issuer = options.issuer ?? this.defaultIssuer;
203
+ const audience = options.audience ?? this.defaultAudience;
204
+ const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
205
+ const algorithms = options.algorithms ?? ["RS256", "ES256"];
206
+ const verifyOptions = {
207
+ algorithms,
208
+ clockTolerance,
209
+ issuer,
210
+ audience
211
+ };
212
+ try {
213
+ const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
214
+ return payload;
215
+ } catch (err) {
216
+ const classified = classifyJoseError(err);
217
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
218
+ }
219
+ }
220
+ /**
221
+ * Decode a JWT without verification. Returns null if malformed.
222
+ */
223
+ decode(token) {
224
+ try {
225
+ const parts = token.split(".");
226
+ if (parts.length < 2) return null;
227
+ const payload = parts[1];
228
+ const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
229
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
230
+ let json;
231
+ if (typeof atob === "function") {
232
+ json = atob(b64);
233
+ } else {
234
+ const { Buffer: Buffer2 } = require("buffer");
235
+ json = Buffer2.from(b64, "base64").toString("utf8");
236
+ }
237
+ try {
238
+ json = decodeURIComponent(escape(json));
239
+ } catch {
240
+ }
241
+ const claims = JSON.parse(json);
242
+ if (!claims || typeof claims !== "object") return null;
243
+ return claims;
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+ /** Check if a token is expired based on the `exp` claim. */
249
+ isExpired(token) {
250
+ const claims = this.decode(token);
251
+ if (!claims?.exp) return true;
252
+ const now = Math.floor(Date.now() / 1e3);
253
+ return claims.exp <= now;
254
+ }
255
+ /** Get the claims from a token without verification. */
256
+ getClaims(token) {
257
+ const claims = this.decode(token);
258
+ if (!claims) {
259
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
260
+ }
261
+ return claims;
262
+ }
263
+ async ensureCache() {
264
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
265
+ return this.jwksCache;
266
+ }
267
+ await this.refreshJwks();
268
+ if (!this.jwksCache) {
269
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
270
+ }
271
+ return this.jwksCache;
272
+ }
273
+ async refreshJwks() {
274
+ if (this.inFlightRefresh) {
275
+ return this.inFlightRefresh;
276
+ }
277
+ this.inFlightRefresh = (async () => {
278
+ try {
279
+ let res;
280
+ try {
281
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
282
+ } catch (err) {
283
+ throw new IQAuthError(
284
+ "network",
285
+ err instanceof Error ? err.message : "JWKS fetch network error",
286
+ void 0,
287
+ err
288
+ );
289
+ }
290
+ if (!res.ok) {
291
+ throw new IQAuthError(
292
+ "jwks_fetch_failed",
293
+ `Failed to fetch JWKS: ${res.status}`,
294
+ res.status
295
+ );
296
+ }
297
+ let jwks;
298
+ try {
299
+ jwks = await res.json();
300
+ } catch (err) {
301
+ throw new IQAuthError(
302
+ "jwks_fetch_failed",
303
+ "Malformed JWKS response: invalid JSON",
304
+ res.status,
305
+ err
306
+ );
307
+ }
308
+ if (!jwks || !Array.isArray(jwks.keys)) {
309
+ throw new IQAuthError(
310
+ "jwks_fetch_failed",
311
+ "Malformed JWKS response: expected { keys: [...] }"
312
+ );
313
+ }
314
+ const byKid = /* @__PURE__ */ new Set();
315
+ for (const key of jwks.keys) {
316
+ 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")) {
317
+ throw new IQAuthError(
318
+ "jwks_fetch_failed",
319
+ "Malformed JWKS response: key missing required fields"
320
+ );
321
+ }
322
+ byKid.add(key.kid);
323
+ }
324
+ const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
325
+ this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
326
+ } finally {
327
+ this.inFlightRefresh = null;
328
+ }
329
+ })();
330
+ return this.inFlightRefresh;
331
+ }
332
+ /** @internal Exposed for testing — clears JWKS cache */
333
+ clearCache() {
334
+ this.jwksCache = null;
335
+ }
336
+ /**
337
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
338
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
339
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
340
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
341
+ */
342
+ async prewarm() {
343
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
344
+ try {
345
+ await this.refreshJwks();
346
+ } catch {
347
+ }
348
+ }
349
+ };
350
+
109
351
  // src/server/handlers.ts
352
+ async function buildUserinfoResponse(claims, opts = {}) {
353
+ const baseUser = {
354
+ sub: claims.sub,
355
+ email: claims.email,
356
+ name: claims.name,
357
+ tenantId: claims.tenantId,
358
+ vendorId: claims.vendorId,
359
+ roles: claims.roles ?? [],
360
+ entitlements: claims.entitlements ?? []
361
+ };
362
+ const enriched = opts.enrich ? await opts.enrich(claims) : null;
363
+ const user = enriched ? { ...baseUser, ...enriched } : baseUser;
364
+ return {
365
+ success: true,
366
+ data: {
367
+ user,
368
+ claims,
369
+ tenantId: claims.tenantId ?? null
370
+ }
371
+ };
372
+ }
373
+ function emitTiming(cfg, event) {
374
+ if (cfg.debug) {
375
+ try {
376
+ console.debug("[iqauth_helper]", event);
377
+ } catch {
378
+ }
379
+ }
380
+ if (cfg.onTimingEvent) {
381
+ try {
382
+ cfg.onTimingEvent(event);
383
+ } catch {
384
+ }
385
+ }
386
+ }
110
387
  var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
111
388
  "TOKEN_REVOKED",
112
389
  "SESSION_REVOKED",
@@ -146,7 +423,11 @@ function resolve(config) {
146
423
  })),
147
424
  appId: parsed.appId,
148
425
  tenantId: parsed.tenantId,
149
- clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
426
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
427
+ debug: config.debug,
428
+ onTimingEvent: config.onTimingEvent,
429
+ signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
430
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
150
431
  };
151
432
  }
152
433
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
@@ -163,15 +444,41 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
163
444
  }
164
445
  function clearCookies(cfg) {
165
446
  return [
166
- makeCookie(cfg, cfg.accessCookieName, "", 0),
167
- makeCookie(cfg, cfg.refreshCookieName, "", 0)
447
+ { ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
448
+ { ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
168
449
  ];
169
450
  }
451
+ var DEFAULT_SIGNOUT_TTL_MS = 6e4;
452
+ var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
453
+ function pruneInMemoryMarkers(now) {
454
+ if (inMemorySignoutMarkers.size === 0) return;
455
+ for (const [k, exp] of inMemorySignoutMarkers) {
456
+ if (exp <= now) inMemorySignoutMarkers.delete(k);
457
+ }
458
+ }
459
+ var defaultSignoutRegistry = {
460
+ mark(token, ttlMs) {
461
+ const now = Date.now();
462
+ pruneInMemoryMarkers(now);
463
+ inMemorySignoutMarkers.set(token, now + ttlMs);
464
+ },
465
+ has(token) {
466
+ const now = Date.now();
467
+ const exp = inMemorySignoutMarkers.get(token);
468
+ if (!exp) return false;
469
+ if (exp <= now) {
470
+ inMemorySignoutMarkers.delete(token);
471
+ return false;
472
+ }
473
+ return true;
474
+ }
475
+ };
170
476
  function serializeCookie(d) {
171
477
  const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
172
478
  parts.push(`Path=${d.path}`);
173
479
  if (d.domain) parts.push(`Domain=${d.domain}`);
174
480
  parts.push(`Max-Age=${d.maxAge}`);
481
+ if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
175
482
  if (d.secure) parts.push("Secure");
176
483
  if (d.httpOnly) parts.push("HttpOnly");
177
484
  parts.push(`SameSite=${d.sameSite}`);
@@ -179,7 +486,9 @@ function serializeCookie(d) {
179
486
  }
180
487
  async function handleCallback(config, input) {
181
488
  const cfg = resolve(config);
489
+ const t0 = Date.now();
182
490
  if (!input.code || !input.redirectUri) {
491
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
183
492
  return {
184
493
  status: 400,
185
494
  body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
@@ -187,6 +496,7 @@ async function handleCallback(config, input) {
187
496
  };
188
497
  }
189
498
  if (!cfg.secretKey) {
499
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
190
500
  return {
191
501
  status: 500,
192
502
  body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
@@ -210,6 +520,7 @@ async function handleCallback(config, input) {
210
520
  });
211
521
  const json = await res.json().catch(() => ({}));
212
522
  if (!res.ok || !json.access_token) {
523
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
213
524
  return {
214
525
  status: res.status || 502,
215
526
  body: {
@@ -229,6 +540,7 @@ async function handleCallback(config, input) {
229
540
  if (json.refresh_token) {
230
541
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
231
542
  }
543
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
232
544
  return {
233
545
  status: 200,
234
546
  body: { success: true, data: { authenticated: true } },
@@ -237,8 +549,18 @@ async function handleCallback(config, input) {
237
549
  }
238
550
  async function handleRefresh(config, input) {
239
551
  const cfg = resolve(config);
552
+ const t0 = Date.now();
240
553
  const refreshToken = input.refreshToken;
554
+ const idemKey = input.idempotencyToken;
555
+ if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
556
+ return {
557
+ status: 401,
558
+ body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
559
+ cookies: clearCookies(cfg)
560
+ };
561
+ }
241
562
  if (!refreshToken) {
563
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
242
564
  return {
243
565
  status: 401,
244
566
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
@@ -254,6 +576,7 @@ async function handleRefresh(config, input) {
254
576
  if (!res.ok || !json.success || !json.data?.accessToken) {
255
577
  const status = res.status || 401;
256
578
  const errorCode = json.error?.code || "TOKEN_INVALID";
579
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
257
580
  const shouldClear = shouldClearCookiesOnFailure(
258
581
  cfg.clearCookiesOnRefreshFailure,
259
582
  status,
@@ -277,6 +600,7 @@ async function handleRefresh(config, input) {
277
600
  if (json.data.refreshToken) {
278
601
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
279
602
  }
603
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
280
604
  return {
281
605
  status: 200,
282
606
  body: { success: true, data: { accessToken: json.data.accessToken } },
@@ -285,6 +609,10 @@ async function handleRefresh(config, input) {
285
609
  }
286
610
  async function handleSignout(config, input) {
287
611
  const cfg = resolve(config);
612
+ const t0 = Date.now();
613
+ if (input.idempotencyToken) {
614
+ await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
615
+ }
288
616
  if (input.accessToken) {
289
617
  try {
290
618
  await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
@@ -306,204 +634,52 @@ async function handleSignout(config, input) {
306
634
  } catch {
307
635
  }
308
636
  }
637
+ emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
309
638
  return {
310
639
  status: 200,
311
640
  body: { success: true, data: { signedOut: true } },
312
641
  cookies: clearCookies(cfg)
313
642
  };
314
643
  }
315
-
316
- // src/modules/tokens.ts
317
- var import_jose = require("jose");
318
- var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
319
- var DEFAULT_TOKEN_ISSUER = [
320
- "https://auth.dispositioniq.com",
321
- "auth.dispositioniq.com"
322
- ];
323
- var DEFAULT_TOKEN_AUDIENCE = [
324
- "dispositioniq",
325
- "iqcapture",
326
- "iqreuse",
327
- "iqvalidate"
328
- ];
329
- var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
330
- function decodeProtectedHeader(token) {
331
- const parts = token.split(".");
332
- if (parts.length < 2) return null;
333
- try {
334
- const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
335
- const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
336
- let json;
337
- if (typeof atob === "function") {
338
- json = atob(b64);
339
- } else {
340
- const { Buffer: Buffer2 } = require("buffer");
341
- json = Buffer2.from(b64, "base64").toString("utf8");
342
- }
343
- return JSON.parse(json);
344
- } catch {
345
- return null;
644
+ var TOKENS_CACHE = /* @__PURE__ */ new Map();
645
+ function getTokensFor(issuer) {
646
+ let m = TOKENS_CACHE.get(issuer);
647
+ if (!m) {
648
+ m = new TokensModule(issuer);
649
+ TOKENS_CACHE.set(issuer, m);
346
650
  }
651
+ return m;
347
652
  }
348
- var TokensModule = class {
349
- constructor(baseUrl, options = {}) {
350
- this.jwksCache = null;
351
- this.inFlightRefresh = null;
352
- this.baseUrl = baseUrl;
353
- this.defaultIssuer = options.issuer ?? DEFAULT_TOKEN_ISSUER;
354
- this.defaultAudience = options.audience ?? DEFAULT_TOKEN_AUDIENCE;
355
- this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
356
- }
357
- /**
358
- * Verify a JWT access token using RS256/ES256 via JWKS from
359
- * `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
360
- * Node, browser, and edge runtimes alike — no `node:crypto` dependency.
361
- * Caches JWKS for 1 hour and refetches once on unknown `kid`.
362
- */
363
- async verify(token, options = {}) {
364
- const header = decodeProtectedHeader(token);
365
- if (!header) {
366
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
367
- }
368
- const kid = header.kid;
369
- if (!kid) {
370
- throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
371
- }
372
- let cache = await this.ensureCache();
373
- if (!cache.byKid.has(kid)) {
374
- this.jwksCache = null;
375
- cache = await this.ensureCache();
376
- }
377
- if (!cache.byKid.has(kid)) {
378
- throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
379
- }
380
- const issuer = options.issuer ?? this.defaultIssuer;
381
- const audience = options.audience ?? this.defaultAudience;
382
- const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
383
- const algorithms = options.algorithms ?? ["RS256", "ES256"];
384
- const verifyOptions = {
385
- algorithms,
386
- clockTolerance,
387
- issuer,
388
- audience
653
+ async function handleUserinfo(config, input) {
654
+ const cfg = resolve(config);
655
+ if (!input.accessToken) {
656
+ return {
657
+ status: 401,
658
+ body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
659
+ cookies: []
389
660
  };
390
- try {
391
- const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
392
- return payload;
393
- } catch (err) {
394
- if (err instanceof import_jose.errors.JWTExpired) {
395
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
396
- }
397
- if (err instanceof import_jose.errors.JOSEError) {
398
- throw new IQAuthError("TOKEN_INVALID", err.message);
399
- }
400
- if (err instanceof Error) {
401
- throw new IQAuthError("TOKEN_INVALID", err.message);
402
- }
403
- throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
404
- }
405
- }
406
- /**
407
- * Decode a JWT without verification. Returns null if malformed.
408
- */
409
- decode(token) {
410
- try {
411
- const parts = token.split(".");
412
- if (parts.length < 2) return null;
413
- const payload = parts[1];
414
- const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
415
- const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
416
- let json;
417
- if (typeof atob === "function") {
418
- json = atob(b64);
419
- } else {
420
- const { Buffer: Buffer2 } = require("buffer");
421
- json = Buffer2.from(b64, "base64").toString("utf8");
422
- }
423
- try {
424
- json = decodeURIComponent(escape(json));
425
- } catch {
426
- }
427
- const claims = JSON.parse(json);
428
- if (!claims || typeof claims !== "object") return null;
429
- return claims;
430
- } catch {
431
- return null;
432
- }
433
661
  }
434
- /** Check if a token is expired based on the `exp` claim. */
435
- isExpired(token) {
436
- const claims = this.decode(token);
437
- if (!claims?.exp) return true;
438
- const now = Math.floor(Date.now() / 1e3);
439
- return claims.exp <= now;
440
- }
441
- /** Get the claims from a token without verification. */
442
- getClaims(token) {
443
- const claims = this.decode(token);
444
- if (!claims) {
445
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
446
- }
447
- return claims;
448
- }
449
- async ensureCache() {
450
- if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
451
- return this.jwksCache;
452
- }
453
- await this.refreshJwks();
454
- if (!this.jwksCache) {
455
- throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
456
- }
457
- return this.jwksCache;
458
- }
459
- async refreshJwks() {
460
- if (this.inFlightRefresh) {
461
- return this.inFlightRefresh;
462
- }
463
- this.inFlightRefresh = (async () => {
464
- try {
465
- const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
466
- if (!res.ok) {
467
- throw new IQAuthError(
468
- "INTERNAL_ERROR",
469
- `Failed to fetch JWKS: ${res.status}`
470
- );
471
- }
472
- let jwks;
473
- try {
474
- jwks = await res.json();
475
- } catch {
476
- throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
477
- }
478
- if (!jwks || !Array.isArray(jwks.keys)) {
479
- throw new IQAuthError(
480
- "INTERNAL_ERROR",
481
- "Malformed JWKS response: expected { keys: [...] }"
482
- );
483
- }
484
- const byKid = /* @__PURE__ */ new Set();
485
- for (const key of jwks.keys) {
486
- 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")) {
487
- throw new IQAuthError(
488
- "INTERNAL_ERROR",
489
- "Malformed JWKS response: key missing required fields"
490
- );
491
- }
492
- byKid.add(key.kid);
493
- }
494
- const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
495
- this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
496
- } finally {
497
- this.inFlightRefresh = null;
498
- }
499
- })();
500
- return this.inFlightRefresh;
501
- }
502
- /** @internal Exposed for testing — clears JWKS cache */
503
- clearCache() {
504
- this.jwksCache = null;
662
+ let claims;
663
+ try {
664
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
665
+ } catch (err) {
666
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
667
+ const message = err instanceof Error ? err.message : "Access token verification failed";
668
+ return {
669
+ status: 401,
670
+ body: { success: false, error: { code, message } },
671
+ cookies: []
672
+ };
505
673
  }
506
- };
674
+ const envelope = await buildUserinfoResponse(claims, {
675
+ enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
676
+ });
677
+ return {
678
+ status: 200,
679
+ body: envelope,
680
+ cookies: []
681
+ };
682
+ }
507
683
 
508
684
  // src/next.ts
509
685
  function readCookieFromHeader(header, name) {
@@ -535,8 +711,19 @@ function handler(options) {
535
711
  return async (req) => {
536
712
  const url = new URL(req.url);
537
713
  const action = url.pathname.split("/").pop();
538
- const body = await req.json().catch(() => ({}));
539
714
  const cookieHeader = req.headers.get("cookie");
715
+ if (action === "me" && req.method === "GET") {
716
+ if (!options.mountUserinfo) {
717
+ return new Response(JSON.stringify({ success: false, error: { code: "NOT_FOUND", message: "userinfo route not enabled" } }), {
718
+ status: 404,
719
+ headers: { "Content-Type": "application/json" }
720
+ });
721
+ }
722
+ const auth = req.headers.get("authorization");
723
+ const accessToken = auth && auth.replace(/^Bearer /i, "") || readCookieFromHeader(cookieHeader, accessCookie);
724
+ return toResponse(await handleUserinfo(helperConfig, { accessToken, req }));
725
+ }
726
+ const body = await req.json().catch(() => ({}));
540
727
  if (action === "callback") {
541
728
  return toResponse(await handleCallback(helperConfig, {
542
729
  code: body.code,
@@ -546,12 +733,15 @@ function handler(options) {
546
733
  }
547
734
  if (action === "refresh") {
548
735
  const refreshToken = body.refreshToken || readCookieFromHeader(cookieHeader, refreshCookie);
549
- return toResponse(await handleRefresh(helperConfig, { refreshToken }));
736
+ const idempotencyToken = req.headers.get("x-iqauth-idempotency") || body.idempotencyToken;
737
+ return toResponse(await handleRefresh(helperConfig, { refreshToken, idempotencyToken: idempotencyToken ?? void 0 }));
550
738
  }
551
739
  if (action === "signout") {
552
740
  const auth = req.headers.get("authorization");
553
741
  const accessToken = auth && auth.replace(/^Bearer /i, "") || readCookieFromHeader(cookieHeader, accessCookie);
554
- return toResponse(await handleSignout(helperConfig, { accessToken, ssoCookieHeader: cookieHeader ?? void 0 }));
742
+ const refreshToken = readCookieFromHeader(cookieHeader, refreshCookie);
743
+ const idempotencyToken = req.headers.get("x-iqauth-idempotency") ?? void 0;
744
+ return toResponse(await handleSignout(helperConfig, { accessToken, refreshToken, idempotencyToken, ssoCookieHeader: cookieHeader ?? void 0 }));
555
745
  }
556
746
  return new Response(JSON.stringify({ success: false, error: { code: "NOT_FOUND", message: `Unknown action: ${action}` } }), {
557
747
  status: 404,