@iqauth/sdk 2.6.4 → 2.8.1

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 (117) 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 +212 -46
  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 +293 -34
  9. package/dist/browser.mjs +5 -5
  10. package/dist/{chunk-BVV54LPI.mjs → chunk-25SSYDIP.mjs} +10 -4
  11. package/dist/{chunk-XAWYUPMO.mjs → chunk-4V7FKOTG.mjs} +242 -22
  12. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  13. package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
  14. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  15. package/dist/chunk-GLXSIGVS.mjs +66 -0
  16. package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
  17. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  18. package/dist/chunk-JRDVUWAL.mjs +46 -0
  19. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  20. package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
  21. package/dist/chunk-VYQ3ETCK.mjs +244 -0
  22. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  23. package/dist/chunk-WHT6WKTY.mjs +3180 -0
  24. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  25. package/dist/chunk-WSH4SW7F.mjs +490 -0
  26. package/dist/{chunk-W3F4JYGP.mjs → chunk-ZLJPABB7.mjs} +139 -23
  27. package/dist/cli/index.js +2 -2
  28. package/dist/cli/index.mjs +2 -2
  29. package/dist/{client-BNQe3AgF.d.ts → client-D8L-PaWr.d.mts} +59 -6
  30. package/dist/{client-kYlJFgPv.d.mts → client-DkPL0EPZ.d.ts} +59 -6
  31. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  32. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  33. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  34. package/dist/{express-CHpfa7D_.d.ts → express-Budysq4h.d.ts} +2 -2
  35. package/dist/{express-B6_1vBYZ.d.mts → express-DDTA3qV1.d.mts} +2 -2
  36. package/dist/express.d.mts +7 -6
  37. package/dist/express.d.ts +7 -6
  38. package/dist/express.js +563 -85
  39. package/dist/express.mjs +73 -34
  40. package/dist/fastify.d.mts +10 -0
  41. package/dist/fastify.d.ts +10 -0
  42. package/dist/fastify.js +589 -65
  43. package/dist/fastify.mjs +101 -11
  44. package/dist/hono.d.mts +10 -0
  45. package/dist/hono.d.ts +10 -0
  46. package/dist/hono.js +566 -65
  47. package/dist/hono.mjs +78 -11
  48. package/dist/index-Cko-d5po.d.mts +1848 -0
  49. package/dist/index-RNqwEcmY.d.ts +1848 -0
  50. package/dist/index.d.mts +56 -8
  51. package/dist/index.d.ts +56 -8
  52. package/dist/index.js +694 -75
  53. package/dist/index.mjs +30 -10
  54. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  55. package/dist/locales.d.mts +1 -1
  56. package/dist/locales.d.ts +1 -1
  57. package/dist/locales.js +36 -0
  58. package/dist/locales.mjs +1 -1
  59. package/dist/mobile.d.mts +77 -7
  60. package/dist/mobile.d.ts +77 -7
  61. package/dist/mobile.js +307 -46
  62. package/dist/mobile.mjs +98 -3
  63. package/dist/next.d.mts +10 -1
  64. package/dist/next.d.ts +10 -1
  65. package/dist/next.js +596 -205
  66. package/dist/next.mjs +83 -10
  67. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
  68. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
  69. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  70. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  71. package/dist/react-permissions.d.mts +52 -0
  72. package/dist/react-permissions.d.ts +52 -0
  73. package/dist/react-permissions.js +239 -0
  74. package/dist/react-permissions.mjs +98 -0
  75. package/dist/react.d.mts +9 -1624
  76. package/dist/react.d.ts +9 -1624
  77. package/dist/react.js +882 -73
  78. package/dist/react.mjs +71 -2631
  79. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  80. package/dist/server/handlers.d.mts +200 -4
  81. package/dist/server/handlers.d.ts +200 -4
  82. package/dist/server/handlers.js +530 -16
  83. package/dist/server/handlers.mjs +14 -3
  84. package/dist/server.d.mts +171 -8
  85. package/dist/server.d.ts +171 -8
  86. package/dist/server.js +579 -61
  87. package/dist/server.mjs +99 -12
  88. package/dist/service.d.mts +4 -4
  89. package/dist/service.d.ts +4 -4
  90. package/dist/service.js +212 -46
  91. package/dist/service.mjs +3 -3
  92. package/dist/{signIn-CiIBTJIh.d.mts → signIn-CReqfXsh.d.mts} +95 -3
  93. package/dist/{signIn-OCr88Zf8.d.ts → signIn-Cfa1GTpO.d.ts} +95 -3
  94. package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
  95. package/dist/test.mjs +3 -3
  96. package/dist/{tokens-DCyzzn8L.d.mts → tokens-9F6ETrzk.d.ts} +9 -2
  97. package/dist/{tokens-aHiGFr_E.d.ts → tokens-B06VtvUi.d.mts} +9 -2
  98. package/dist/{types-DZAflmmq.d.mts → types-Bn8O-OEd.d.mts} +164 -11
  99. package/dist/{types-DZAflmmq.d.ts → types-Bn8O-OEd.d.ts} +164 -11
  100. package/dist/{types-6bNdxesb.d.ts → types-DnU2LhXR.d.mts} +7 -1
  101. package/dist/{types-6bNdxesb.d.mts → types-DnU2LhXR.d.ts} +7 -1
  102. package/dist/webhooks.d.mts +113 -17
  103. package/dist/webhooks.d.ts +113 -17
  104. package/dist/webhooks.js +179 -15
  105. package/dist/webhooks.mjs +7 -1
  106. package/dist/ws.d.mts +2 -2
  107. package/dist/ws.d.ts +2 -2
  108. package/dist/ws.js +80 -30
  109. package/dist/ws.mjs +4 -4
  110. package/docs/error-handling.md +101 -0
  111. package/docs/guides/effective-permissions.md +171 -0
  112. package/docs/guides/invitations.md +65 -0
  113. package/package.json +19 -4
  114. package/dist/chunk-6TDJJER7.mjs +0 -217
  115. package/dist/chunk-UKZLOHZG.mjs +0 -83
  116. package/dist/errors-CDdl24MP.d.mts +0 -52
  117. package/dist/errors-CDdl24MP.d.ts +0 -52
@@ -20,21 +20,43 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/server/handlers.ts
21
21
  var handlers_exports = {};
22
22
  __export(handlers_exports, {
23
+ __resetSignoutMarkersForTests: () => __resetSignoutMarkersForTests,
24
+ __resetSignoutRegistryWarningForTests: () => __resetSignoutRegistryWarningForTests,
25
+ buildUserinfoResponse: () => buildUserinfoResponse,
26
+ createInMemorySignoutRegistry: () => createInMemorySignoutRegistry,
23
27
  handleCallback: () => handleCallback,
24
28
  handleRefresh: () => handleRefresh,
25
29
  handleSignout: () => handleSignout,
30
+ handleUserinfo: () => handleUserinfo,
26
31
  serializeCookie: () => serializeCookie
27
32
  });
28
33
  module.exports = __toCommonJS(handlers_exports);
29
34
 
30
35
  // src/errors.ts
31
- var IQAuthError = class extends Error {
32
- constructor(code, message, status, raw) {
36
+ var IQAuthError = class _IQAuthError extends Error {
37
+ constructor(code, message, status, cause) {
33
38
  super(message);
34
39
  this.name = "IQAuthError";
35
40
  this.code = code;
36
41
  this.status = status;
37
- this.raw = raw;
42
+ this.cause = cause;
43
+ this.raw = cause;
44
+ }
45
+ /**
46
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
47
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
48
+ */
49
+ static isIQAuthError(value) {
50
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
51
+ }
52
+ /**
53
+ * Type-narrowed code check. Lets callers write
54
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
55
+ * taxonomy without losing the ability to handle server codes via
56
+ * `err.code === "TOKEN_REVOKED"`.
57
+ */
58
+ is(code) {
59
+ return this.code === code;
38
60
  }
39
61
  };
40
62
 
@@ -67,14 +89,14 @@ function assertPublishableKey(raw, opts) {
67
89
  const ctx = opts?.context ? `${opts.context}: ` : "";
68
90
  if (typeof raw !== "string" || raw.length === 0) {
69
91
  throw new IQAuthError(
70
- "CONFIG_INVALID",
92
+ "config_invalid",
71
93
  `${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.`
72
94
  );
73
95
  }
74
96
  const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
75
97
  if (!shapeMatch) {
76
98
  throw new IQAuthError(
77
- "CONFIG_INVALID",
99
+ "config_invalid",
78
100
  `${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.`
79
101
  );
80
102
  }
@@ -83,19 +105,19 @@ function assertPublishableKey(raw, opts) {
83
105
  decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
84
106
  } catch {
85
107
  throw new IQAuthError(
86
- "CONFIG_INVALID",
108
+ "config_invalid",
87
109
  `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
88
110
  );
89
111
  }
90
112
  if (!isPublishableKeyPayload(decoded)) {
91
113
  throw new IQAuthError(
92
- "CONFIG_INVALID",
114
+ "config_invalid",
93
115
  `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
94
116
  );
95
117
  }
96
118
  if (!isValidIssuerUrl(decoded.iss)) {
97
119
  throw new IQAuthError(
98
- "CONFIG_INVALID",
120
+ "config_invalid",
99
121
  `${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.`
100
122
  );
101
123
  }
@@ -107,7 +129,271 @@ function isPublishableKeyPayload(value) {
107
129
  return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
108
130
  }
109
131
 
132
+ // src/modules/tokens.ts
133
+ var import_jose = require("jose");
134
+ var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
135
+ var DEFAULT_TOKEN_ISSUER = [
136
+ "https://auth.dispositioniq.com",
137
+ "auth.dispositioniq.com"
138
+ ];
139
+ var DEFAULT_TOKEN_AUDIENCE = [
140
+ "dispositioniq",
141
+ "iqcapture",
142
+ "iqreuse",
143
+ "iqvalidate"
144
+ ];
145
+ var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
146
+ function classifyJoseError(err) {
147
+ if (err instanceof import_jose.errors.JWTExpired) {
148
+ return { code: "token_expired", message: "Token has expired" };
149
+ }
150
+ if (err instanceof import_jose.errors.JOSEError) {
151
+ return { code: "token_invalid", message: err.message };
152
+ }
153
+ if (err instanceof Error) {
154
+ return { code: "token_invalid", message: err.message };
155
+ }
156
+ return { code: "token_invalid", message: "Token verification failed" };
157
+ }
158
+ function decodeProtectedHeader(token) {
159
+ const parts = token.split(".");
160
+ if (parts.length < 2) return null;
161
+ try {
162
+ const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
163
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
164
+ let json;
165
+ if (typeof atob === "function") {
166
+ json = atob(b64);
167
+ } else {
168
+ const { Buffer: Buffer2 } = require("buffer");
169
+ json = Buffer2.from(b64, "base64").toString("utf8");
170
+ }
171
+ return JSON.parse(json);
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ var TokensModule = class {
177
+ constructor(baseUrl, options = {}) {
178
+ this.jwksCache = null;
179
+ this.inFlightRefresh = null;
180
+ this.baseUrl = baseUrl;
181
+ this.defaultIssuer = options.issuer ?? DEFAULT_TOKEN_ISSUER;
182
+ this.defaultAudience = options.audience ?? DEFAULT_TOKEN_AUDIENCE;
183
+ this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
184
+ }
185
+ /**
186
+ * Verify a JWT access token using RS256/ES256 via JWKS from
187
+ * `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
188
+ * Node, browser, and edge runtimes alike — no `node:crypto` dependency.
189
+ * Caches JWKS for 1 hour and refetches once on unknown `kid`.
190
+ */
191
+ async verify(token, options = {}) {
192
+ const header = decodeProtectedHeader(token);
193
+ if (!header) {
194
+ throw new IQAuthError("token_invalid", "Unable to decode token");
195
+ }
196
+ const kid = header.kid;
197
+ if (!kid) {
198
+ throw new IQAuthError("token_invalid", "Token missing kid header");
199
+ }
200
+ let cache = await this.ensureCache();
201
+ if (!cache.byKid.has(kid)) {
202
+ this.jwksCache = null;
203
+ cache = await this.ensureCache();
204
+ }
205
+ if (!cache.byKid.has(kid)) {
206
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
207
+ }
208
+ const issuer = options.issuer ?? this.defaultIssuer;
209
+ const audience = options.audience ?? this.defaultAudience;
210
+ const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
211
+ const algorithms = options.algorithms ?? ["RS256", "ES256"];
212
+ const verifyOptions = {
213
+ algorithms,
214
+ clockTolerance,
215
+ issuer,
216
+ audience
217
+ };
218
+ try {
219
+ const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
220
+ return payload;
221
+ } catch (err) {
222
+ const classified = classifyJoseError(err);
223
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
224
+ }
225
+ }
226
+ /**
227
+ * Decode a JWT without verification. Returns null if malformed.
228
+ */
229
+ decode(token) {
230
+ try {
231
+ const parts = token.split(".");
232
+ if (parts.length < 2) return null;
233
+ const payload = parts[1];
234
+ const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
235
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
236
+ let json;
237
+ if (typeof atob === "function") {
238
+ json = atob(b64);
239
+ } else {
240
+ const { Buffer: Buffer2 } = require("buffer");
241
+ json = Buffer2.from(b64, "base64").toString("utf8");
242
+ }
243
+ try {
244
+ json = decodeURIComponent(escape(json));
245
+ } catch {
246
+ }
247
+ const claims = JSON.parse(json);
248
+ if (!claims || typeof claims !== "object") return null;
249
+ return claims;
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ /** Check if a token is expired based on the `exp` claim. */
255
+ isExpired(token) {
256
+ const claims = this.decode(token);
257
+ if (!claims?.exp) return true;
258
+ const now = Math.floor(Date.now() / 1e3);
259
+ return claims.exp <= now;
260
+ }
261
+ /** Get the claims from a token without verification. */
262
+ getClaims(token) {
263
+ const claims = this.decode(token);
264
+ if (!claims) {
265
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
266
+ }
267
+ return claims;
268
+ }
269
+ async ensureCache() {
270
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
271
+ return this.jwksCache;
272
+ }
273
+ await this.refreshJwks();
274
+ if (!this.jwksCache) {
275
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
276
+ }
277
+ return this.jwksCache;
278
+ }
279
+ async refreshJwks() {
280
+ if (this.inFlightRefresh) {
281
+ return this.inFlightRefresh;
282
+ }
283
+ this.inFlightRefresh = (async () => {
284
+ try {
285
+ let res;
286
+ try {
287
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
288
+ } catch (err) {
289
+ throw new IQAuthError(
290
+ "network",
291
+ err instanceof Error ? err.message : "JWKS fetch network error",
292
+ void 0,
293
+ err
294
+ );
295
+ }
296
+ if (!res.ok) {
297
+ throw new IQAuthError(
298
+ "jwks_fetch_failed",
299
+ `Failed to fetch JWKS: ${res.status}`,
300
+ res.status
301
+ );
302
+ }
303
+ let jwks;
304
+ try {
305
+ jwks = await res.json();
306
+ } catch (err) {
307
+ throw new IQAuthError(
308
+ "jwks_fetch_failed",
309
+ "Malformed JWKS response: invalid JSON",
310
+ res.status,
311
+ err
312
+ );
313
+ }
314
+ if (!jwks || !Array.isArray(jwks.keys)) {
315
+ throw new IQAuthError(
316
+ "jwks_fetch_failed",
317
+ "Malformed JWKS response: expected { keys: [...] }"
318
+ );
319
+ }
320
+ const byKid = /* @__PURE__ */ new Set();
321
+ for (const key of jwks.keys) {
322
+ 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")) {
323
+ throw new IQAuthError(
324
+ "jwks_fetch_failed",
325
+ "Malformed JWKS response: key missing required fields"
326
+ );
327
+ }
328
+ byKid.add(key.kid);
329
+ }
330
+ const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
331
+ this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
332
+ } finally {
333
+ this.inFlightRefresh = null;
334
+ }
335
+ })();
336
+ return this.inFlightRefresh;
337
+ }
338
+ /** @internal Exposed for testing — clears JWKS cache */
339
+ clearCache() {
340
+ this.jwksCache = null;
341
+ }
342
+ /**
343
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
344
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
345
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
346
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
347
+ */
348
+ async prewarm() {
349
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
350
+ try {
351
+ await this.refreshJwks();
352
+ } catch {
353
+ }
354
+ }
355
+ };
356
+
110
357
  // src/server/handlers.ts
358
+ async function buildUserinfoResponse(claims, opts = {}) {
359
+ const baseUser = {
360
+ sub: claims.sub,
361
+ email: claims.email,
362
+ name: claims.name,
363
+ tenantId: claims.tenantId,
364
+ vendorId: claims.vendorId,
365
+ roles: claims.roles ?? [],
366
+ entitlements: claims.entitlements ?? [],
367
+ // Task #171 — project the active source/client scope onto the userinfo
368
+ // payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
369
+ // expose it without consumers having to re-decode the JWT.
370
+ ...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
371
+ };
372
+ const enriched = opts.enrich ? await opts.enrich(claims) : null;
373
+ const user = enriched ? { ...baseUser, ...enriched } : baseUser;
374
+ return {
375
+ success: true,
376
+ data: {
377
+ user,
378
+ claims,
379
+ tenantId: claims.tenantId ?? null
380
+ }
381
+ };
382
+ }
383
+ function emitTiming(cfg, event) {
384
+ if (cfg.debug) {
385
+ try {
386
+ console.debug("[iqauth_helper]", event);
387
+ } catch {
388
+ }
389
+ }
390
+ if (cfg.onTimingEvent) {
391
+ try {
392
+ cfg.onTimingEvent(event);
393
+ } catch {
394
+ }
395
+ }
396
+ }
111
397
  var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
112
398
  "TOKEN_REVOKED",
113
399
  "SESSION_REVOKED",
@@ -126,19 +412,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
126
412
  }
127
413
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
128
414
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
415
+ function assertCookiePrefixInvariants(name, secure, path, domain) {
416
+ if (name.startsWith("__Host-")) {
417
+ if (!secure) {
418
+ throw new IQAuthError(
419
+ "config_invalid",
420
+ `Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
421
+ );
422
+ }
423
+ if (path !== "/") {
424
+ throw new IQAuthError(
425
+ "config_invalid",
426
+ `Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
427
+ );
428
+ }
429
+ if (domain) {
430
+ throw new IQAuthError(
431
+ "config_invalid",
432
+ `Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
433
+ );
434
+ }
435
+ } else if (name.startsWith("__Secure-") && !secure) {
436
+ throw new IQAuthError(
437
+ "config_invalid",
438
+ `Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
439
+ );
440
+ }
441
+ }
129
442
  function resolve(config) {
130
443
  const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
131
444
  const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
445
+ maybeWarnDefaultSignoutRegistry(config);
446
+ const secure = config.secure ?? true;
447
+ if (config.secure === false && config.allowInsecureCookies !== true) {
448
+ throw new IQAuthError(
449
+ "config_invalid",
450
+ "Refusing to issue auth cookies with secure:false \u2014 this exposes session cookies over plaintext HTTP. For local HTTP development, set allowInsecureCookies:true to acknowledge the risk. Production MUST use HTTPS with secure cookies."
451
+ );
452
+ }
453
+ const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
454
+ const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
455
+ const stateCookieName = config.stateCookieName ?? "iqauth_state";
456
+ const cookiePath = config.cookiePath ?? "/";
457
+ const cookieDomain = config.cookieDomain;
458
+ for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
459
+ assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
460
+ }
132
461
  return {
133
462
  publishableKey: config.publishableKey,
134
463
  secretKey: config.secretKey,
135
464
  issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
136
- accessCookieName: config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at",
137
- refreshCookieName: config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt",
138
- cookieDomain: config.cookieDomain,
465
+ accessCookieName,
466
+ refreshCookieName,
467
+ cookieDomain,
139
468
  sameSite: config.sameSite ?? "lax",
140
- secure: config.secure ?? true,
141
- cookiePath: config.cookiePath ?? "/",
469
+ secure,
470
+ cookiePath,
142
471
  tokenPath: config.tokenPath ?? "/oidc/token",
143
472
  refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
144
473
  logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
@@ -147,9 +476,23 @@ function resolve(config) {
147
476
  })),
148
477
  appId: parsed.appId,
149
478
  tenantId: parsed.tenantId,
150
- clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
479
+ clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
480
+ debug: config.debug,
481
+ onTimingEvent: config.onTimingEvent,
482
+ signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
483
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
484
+ requireOAuthState: config.requireOAuthState ?? true,
485
+ stateCookieName: config.stateCookieName ?? "iqauth_state"
151
486
  };
152
487
  }
488
+ function timingSafeEqualStr(a, b) {
489
+ const len = Math.max(a.length, b.length);
490
+ let diff = a.length ^ b.length;
491
+ for (let i = 0; i < len; i++) {
492
+ diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
493
+ }
494
+ return diff === 0;
495
+ }
153
496
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
154
497
  return {
155
498
  name,
@@ -164,15 +507,79 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
164
507
  }
165
508
  function clearCookies(cfg) {
166
509
  return [
167
- makeCookie(cfg, cfg.accessCookieName, "", 0),
168
- makeCookie(cfg, cfg.refreshCookieName, "", 0)
510
+ { ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
511
+ { ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
169
512
  ];
170
513
  }
514
+ function clearStateCookie(cfg) {
515
+ return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
516
+ }
517
+ var DEFAULT_SIGNOUT_TTL_MS = 6e4;
518
+ var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
519
+ function pruneInMemoryMarkers(now) {
520
+ if (inMemorySignoutMarkers.size === 0) return;
521
+ for (const [k, exp] of inMemorySignoutMarkers) {
522
+ if (exp <= now) inMemorySignoutMarkers.delete(k);
523
+ }
524
+ }
525
+ var defaultSignoutRegistry = {
526
+ mark(token, ttlMs) {
527
+ const now = Date.now();
528
+ pruneInMemoryMarkers(now);
529
+ inMemorySignoutMarkers.set(token, now + ttlMs);
530
+ },
531
+ has(token) {
532
+ const now = Date.now();
533
+ const exp = inMemorySignoutMarkers.get(token);
534
+ if (!exp) return false;
535
+ if (exp <= now) {
536
+ inMemorySignoutMarkers.delete(token);
537
+ return false;
538
+ }
539
+ return true;
540
+ }
541
+ };
542
+ var warnedDefaultSignoutRegistry = false;
543
+ function maybeWarnDefaultSignoutRegistry(config) {
544
+ if (warnedDefaultSignoutRegistry) return;
545
+ if (config.signoutRegistry) return;
546
+ warnedDefaultSignoutRegistry = true;
547
+ console.warn(
548
+ "[IQAuth] Using the in-memory signout registry (process-local). Signout idempotency is NOT shared across instances \u2014 in a multi-replica deployment a /refresh racing a /signout on another replica can reissue cookies after sign-out. Plug a shared backend (e.g. Redis) into IQAuthHelperConfig.signoutRegistry to fix this and silence this warning."
549
+ );
550
+ }
551
+ function __resetSignoutMarkersForTests() {
552
+ inMemorySignoutMarkers.clear();
553
+ }
554
+ function __resetSignoutRegistryWarningForTests() {
555
+ warnedDefaultSignoutRegistry = false;
556
+ }
557
+ function createInMemorySignoutRegistry() {
558
+ const store = /* @__PURE__ */ new Map();
559
+ return {
560
+ mark(token, ttlMs) {
561
+ const now = Date.now();
562
+ for (const [k, exp] of store) if (exp <= now) store.delete(k);
563
+ store.set(token, now + ttlMs);
564
+ },
565
+ has(token) {
566
+ const now = Date.now();
567
+ const exp = store.get(token);
568
+ if (!exp) return false;
569
+ if (exp <= now) {
570
+ store.delete(token);
571
+ return false;
572
+ }
573
+ return true;
574
+ }
575
+ };
576
+ }
171
577
  function serializeCookie(d) {
172
578
  const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
173
579
  parts.push(`Path=${d.path}`);
174
580
  if (d.domain) parts.push(`Domain=${d.domain}`);
175
581
  parts.push(`Max-Age=${d.maxAge}`);
582
+ if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
176
583
  if (d.secure) parts.push("Secure");
177
584
  if (d.httpOnly) parts.push("HttpOnly");
178
585
  parts.push(`SameSite=${d.sameSite}`);
@@ -180,14 +587,34 @@ function serializeCookie(d) {
180
587
  }
181
588
  async function handleCallback(config, input) {
182
589
  const cfg = resolve(config);
590
+ const t0 = Date.now();
183
591
  if (!input.code || !input.redirectUri) {
592
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
184
593
  return {
185
594
  status: 400,
186
595
  body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
187
596
  cookies: []
188
597
  };
189
598
  }
599
+ const provided = input.state;
600
+ const expected = input.expectedState;
601
+ const stateOk = cfg.requireOAuthState ? !!expected && !!provided && timingSafeEqualStr(provided, expected) : !expected || !!provided && timingSafeEqualStr(provided, expected);
602
+ if (!stateOk) {
603
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "STATE_MISMATCH" });
604
+ return {
605
+ status: 400,
606
+ body: {
607
+ success: false,
608
+ error: {
609
+ code: "STATE_MISMATCH",
610
+ message: "OAuth state validation failed; the sign-in could not be verified as originating from this browser."
611
+ }
612
+ },
613
+ cookies: [clearStateCookie(cfg)]
614
+ };
615
+ }
190
616
  if (!cfg.secretKey) {
617
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
191
618
  return {
192
619
  status: 500,
193
620
  body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
@@ -211,6 +638,7 @@ async function handleCallback(config, input) {
211
638
  });
212
639
  const json = await res.json().catch(() => ({}));
213
640
  if (!res.ok || !json.access_token) {
641
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
214
642
  return {
215
643
  status: res.status || 502,
216
644
  body: {
@@ -223,6 +651,26 @@ async function handleCallback(config, input) {
223
651
  cookies: []
224
652
  };
225
653
  }
654
+ try {
655
+ await getTokensFor(cfg.issuer).verify(json.access_token, {
656
+ issuer: cfg.issuer,
657
+ ...config.verify
658
+ });
659
+ } catch (err) {
660
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
661
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code });
662
+ return {
663
+ status: 502,
664
+ body: {
665
+ success: false,
666
+ error: {
667
+ code: "ACCESS_TOKEN_VERIFICATION_FAILED",
668
+ message: "The issuer returned an access token that failed verification; no session was established."
669
+ }
670
+ },
671
+ cookies: []
672
+ };
673
+ }
226
674
  const cookies = [];
227
675
  cookies.push(
228
676
  makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
@@ -230,6 +678,8 @@ async function handleCallback(config, input) {
230
678
  if (json.refresh_token) {
231
679
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
232
680
  }
681
+ cookies.push(clearStateCookie(cfg));
682
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
233
683
  return {
234
684
  status: 200,
235
685
  body: { success: true, data: { authenticated: true } },
@@ -238,8 +688,18 @@ async function handleCallback(config, input) {
238
688
  }
239
689
  async function handleRefresh(config, input) {
240
690
  const cfg = resolve(config);
691
+ const t0 = Date.now();
241
692
  const refreshToken = input.refreshToken;
693
+ const idemKey = input.idempotencyToken;
694
+ if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
695
+ return {
696
+ status: 401,
697
+ body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
698
+ cookies: clearCookies(cfg)
699
+ };
700
+ }
242
701
  if (!refreshToken) {
702
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
243
703
  return {
244
704
  status: 401,
245
705
  body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
@@ -255,6 +715,7 @@ async function handleRefresh(config, input) {
255
715
  if (!res.ok || !json.success || !json.data?.accessToken) {
256
716
  const status = res.status || 401;
257
717
  const errorCode = json.error?.code || "TOKEN_INVALID";
718
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
258
719
  const shouldClear = shouldClearCookiesOnFailure(
259
720
  cfg.clearCookiesOnRefreshFailure,
260
721
  status,
@@ -278,6 +739,7 @@ async function handleRefresh(config, input) {
278
739
  if (json.data.refreshToken) {
279
740
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
280
741
  }
742
+ emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
281
743
  return {
282
744
  status: 200,
283
745
  body: { success: true, data: { accessToken: json.data.accessToken } },
@@ -286,6 +748,10 @@ async function handleRefresh(config, input) {
286
748
  }
287
749
  async function handleSignout(config, input) {
288
750
  const cfg = resolve(config);
751
+ const t0 = Date.now();
752
+ if (input.idempotencyToken) {
753
+ await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
754
+ }
289
755
  if (input.accessToken) {
290
756
  try {
291
757
  await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
@@ -307,16 +773,64 @@ async function handleSignout(config, input) {
307
773
  } catch {
308
774
  }
309
775
  }
776
+ emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
310
777
  return {
311
778
  status: 200,
312
779
  body: { success: true, data: { signedOut: true } },
313
780
  cookies: clearCookies(cfg)
314
781
  };
315
782
  }
783
+ var TOKENS_CACHE = /* @__PURE__ */ new Map();
784
+ function getTokensFor(issuer) {
785
+ let m = TOKENS_CACHE.get(issuer);
786
+ if (!m) {
787
+ m = new TokensModule(issuer);
788
+ TOKENS_CACHE.set(issuer, m);
789
+ }
790
+ return m;
791
+ }
792
+ async function handleUserinfo(config, input) {
793
+ const cfg = resolve(config);
794
+ if (!input.accessToken) {
795
+ return {
796
+ status: 401,
797
+ body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
798
+ cookies: []
799
+ };
800
+ }
801
+ let claims;
802
+ try {
803
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
804
+ issuer: cfg.issuer,
805
+ ...config.verify
806
+ });
807
+ } catch (err) {
808
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
809
+ const message = err instanceof Error ? err.message : "Access token verification failed";
810
+ return {
811
+ status: 401,
812
+ body: { success: false, error: { code, message } },
813
+ cookies: []
814
+ };
815
+ }
816
+ const envelope = await buildUserinfoResponse(claims, {
817
+ enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
818
+ });
819
+ return {
820
+ status: 200,
821
+ body: envelope,
822
+ cookies: []
823
+ };
824
+ }
316
825
  // Annotate the CommonJS export names for ESM import in node:
317
826
  0 && (module.exports = {
827
+ __resetSignoutMarkersForTests,
828
+ __resetSignoutRegistryWarningForTests,
829
+ buildUserinfoResponse,
830
+ createInMemorySignoutRegistry,
318
831
  handleCallback,
319
832
  handleRefresh,
320
833
  handleSignout,
834
+ handleUserinfo,
321
835
  serializeCookie
322
836
  });