@iqauth/sdk 2.7.0 → 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 (88) hide show
  1. package/dist/browser-session.d.mts +3 -3
  2. package/dist/browser-session.d.ts +3 -3
  3. package/dist/browser-session.js +31 -5
  4. package/dist/browser-session.mjs +1 -1
  5. package/dist/browser.d.mts +3 -3
  6. package/dist/browser.d.ts +3 -3
  7. package/dist/browser.js +23 -3
  8. package/dist/browser.mjs +1 -1
  9. package/dist/{chunk-YVALAG3B.mjs → chunk-25SSYDIP.mjs} +1 -1
  10. package/dist/{chunk-RTJAIBXY.mjs → chunk-4V7FKOTG.mjs} +23 -3
  11. package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
  12. package/dist/chunk-JRDVUWAL.mjs +46 -0
  13. package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
  14. package/dist/{chunk-PMAFENVI.mjs → chunk-VYQ3ETCK.mjs} +27 -12
  15. package/dist/{chunk-RR2MGPTK.mjs → chunk-WHT6WKTY.mjs} +539 -83
  16. package/dist/{chunk-RUJXRTEW.mjs → chunk-WSH4SW7F.mjs} +122 -8
  17. package/dist/{chunk-JXQI62A7.mjs → chunk-ZLJPABB7.mjs} +31 -5
  18. package/dist/{client-BGFnBpfc.d.mts → client-D8L-PaWr.d.mts} +14 -4
  19. package/dist/{client-CDQ21LvW.d.ts → client-DkPL0EPZ.d.ts} +14 -4
  20. package/dist/{express-Piv2WhWM.d.ts → express-Budysq4h.d.ts} +2 -2
  21. package/dist/{express-CVNQEkOr.d.mts → express-DDTA3qV1.d.mts} +2 -2
  22. package/dist/express.d.mts +5 -5
  23. package/dist/express.d.ts +5 -5
  24. package/dist/express.js +217 -36
  25. package/dist/express.mjs +38 -26
  26. package/dist/fastify.d.mts +10 -2
  27. package/dist/fastify.d.ts +10 -2
  28. package/dist/fastify.js +260 -16
  29. package/dist/fastify.mjs +80 -5
  30. package/dist/hono.d.mts +10 -2
  31. package/dist/hono.d.ts +10 -2
  32. package/dist/hono.js +240 -16
  33. package/dist/hono.mjs +60 -5
  34. package/dist/{index-5KSZEnDe.d.ts → index-Cko-d5po.d.mts} +227 -5
  35. package/dist/{index-CKoZHAoc.d.mts → index-RNqwEcmY.d.ts} +227 -5
  36. package/dist/index.d.mts +5 -5
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +149 -26
  39. package/dist/index.mjs +5 -5
  40. package/dist/locales.d.mts +1 -1
  41. package/dist/locales.d.ts +1 -1
  42. package/dist/locales.js +36 -0
  43. package/dist/locales.mjs +1 -1
  44. package/dist/mobile.d.mts +3 -3
  45. package/dist/mobile.d.ts +3 -3
  46. package/dist/mobile.js +31 -5
  47. package/dist/mobile.mjs +1 -1
  48. package/dist/next.d.mts +10 -2
  49. package/dist/next.d.ts +10 -2
  50. package/dist/next.js +212 -11
  51. package/dist/next.mjs +62 -4
  52. package/dist/{provisioningBridge-M5G47LWO.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
  53. package/dist/{provisioningBridge-CGpMRie4.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
  54. package/dist/react-permissions.d.mts +4 -4
  55. package/dist/react-permissions.d.ts +4 -4
  56. package/dist/react-permissions.mjs +4 -3
  57. package/dist/react.d.mts +4 -4
  58. package/dist/react.d.ts +4 -4
  59. package/dist/react.js +570 -41
  60. package/dist/react.mjs +19 -5
  61. package/dist/server/handlers.d.mts +56 -5
  62. package/dist/server/handlers.d.ts +56 -5
  63. package/dist/server/handlers.js +123 -8
  64. package/dist/server/handlers.mjs +3 -1
  65. package/dist/server.d.mts +28 -8
  66. package/dist/server.d.ts +28 -8
  67. package/dist/server.js +176 -14
  68. package/dist/server.mjs +9 -4
  69. package/dist/service.d.mts +3 -3
  70. package/dist/service.d.ts +3 -3
  71. package/dist/service.js +31 -5
  72. package/dist/service.mjs +1 -1
  73. package/dist/{signIn-T-CZ6t6r.d.mts → signIn-CReqfXsh.d.mts} +18 -1
  74. package/dist/{signIn-BLFnz8SV.d.ts → signIn-Cfa1GTpO.d.ts} +18 -1
  75. package/dist/{tokens-Bqhmqq_R.d.ts → tokens-9F6ETrzk.d.ts} +1 -1
  76. package/dist/{tokens-CITeoG6P.d.mts → tokens-B06VtvUi.d.mts} +1 -1
  77. package/dist/{types-XOV9XPVi.d.mts → types-Bn8O-OEd.d.mts} +66 -2
  78. package/dist/{types-XOV9XPVi.d.ts → types-Bn8O-OEd.d.ts} +66 -2
  79. package/dist/{types-BdQ2lqfT.d.mts → types-DnU2LhXR.d.mts} +6 -0
  80. package/dist/{types-BdQ2lqfT.d.ts → types-DnU2LhXR.d.ts} +6 -0
  81. package/dist/webhooks.d.mts +22 -9
  82. package/dist/webhooks.d.ts +22 -9
  83. package/dist/webhooks.js +27 -12
  84. package/dist/webhooks.mjs +1 -1
  85. package/dist/ws.d.mts +2 -2
  86. package/dist/ws.d.ts +2 -2
  87. package/docs/guides/invitations.md +65 -0
  88. package/package.json +7 -2
package/dist/fastify.js CHANGED
@@ -296,17 +296,27 @@ function parseLoginResponse(data, browserSessionMode) {
296
296
  tenants: data.tenants
297
297
  };
298
298
  }
299
+ if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
300
+ return {
301
+ status: "scope_selection",
302
+ scopeSelectionToken: data.scopeSelectionToken,
303
+ tenantId: data.tenantId,
304
+ scopes: data.scopes
305
+ };
306
+ }
299
307
  throw new Error("Unexpected login response shape");
300
308
  }
301
309
  var AuthModule = class {
302
310
  constructor(http) {
303
311
  this.http = http;
304
312
  }
305
- async login(email, password) {
313
+ async login(email, password, opts) {
314
+ const body = { email, password };
315
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
306
316
  const data = await this.http.request(
307
317
  "POST",
308
318
  "/api/v1/auth/login",
309
- { email, password },
319
+ body,
310
320
  { skipAutoRefresh: true }
311
321
  );
312
322
  return parseLoginResponse(data, this.http.isBrowserSession());
@@ -344,13 +354,29 @@ var AuthModule = class {
344
354
  method
345
355
  }, { skipAutoRefresh: true });
346
356
  }
347
- async selectTenant(tenantSelectionToken, tenantId) {
357
+ async selectTenant(tenantSelectionToken, tenantId, opts) {
358
+ const body = { tenantSelectionToken, tenantId };
359
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
348
360
  const data = await this.http.request(
349
361
  "POST",
350
362
  "/api/v1/auth/select-tenant",
363
+ body,
364
+ { skipAutoRefresh: true }
365
+ );
366
+ return parseLoginResponse(data, this.http.isBrowserSession());
367
+ }
368
+ /**
369
+ * Task #171 — redeem a scope-selection token + chosen membership for a
370
+ * real authenticated session. `membershipId` must be one of the scopes
371
+ * returned in the prior `scope_selection` envelope.
372
+ */
373
+ async selectScope(scopeSelectionToken, membershipId) {
374
+ const data = await this.http.request(
375
+ "POST",
376
+ "/api/v1/auth/select-scope",
351
377
  {
352
- tenantSelectionToken,
353
- tenantId
378
+ scopeSelectionToken,
379
+ membershipId
354
380
  },
355
381
  { skipAutoRefresh: true }
356
382
  );
@@ -1993,7 +2019,11 @@ async function buildUserinfoResponse(claims, opts = {}) {
1993
2019
  tenantId: claims.tenantId,
1994
2020
  vendorId: claims.vendorId,
1995
2021
  roles: claims.roles ?? [],
1996
- entitlements: claims.entitlements ?? []
2022
+ entitlements: claims.entitlements ?? [],
2023
+ // Task #171 — project the active source/client scope onto the userinfo
2024
+ // payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
2025
+ // expose it without consumers having to re-decode the JWT.
2026
+ ...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
1997
2027
  };
1998
2028
  const enriched = opts.enrich ? await opts.enrich(claims) : null;
1999
2029
  const user = enriched ? { ...baseUser, ...enriched } : baseUser;
@@ -2038,19 +2068,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
2038
2068
  }
2039
2069
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
2040
2070
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
2071
+ function assertCookiePrefixInvariants(name, secure, path, domain) {
2072
+ if (name.startsWith("__Host-")) {
2073
+ if (!secure) {
2074
+ throw new IQAuthError(
2075
+ "config_invalid",
2076
+ `Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
2077
+ );
2078
+ }
2079
+ if (path !== "/") {
2080
+ throw new IQAuthError(
2081
+ "config_invalid",
2082
+ `Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
2083
+ );
2084
+ }
2085
+ if (domain) {
2086
+ throw new IQAuthError(
2087
+ "config_invalid",
2088
+ `Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
2089
+ );
2090
+ }
2091
+ } else if (name.startsWith("__Secure-") && !secure) {
2092
+ throw new IQAuthError(
2093
+ "config_invalid",
2094
+ `Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
2095
+ );
2096
+ }
2097
+ }
2041
2098
  function resolve(config) {
2042
2099
  const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
2043
2100
  const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
2101
+ maybeWarnDefaultSignoutRegistry(config);
2102
+ const secure = config.secure ?? true;
2103
+ if (config.secure === false && config.allowInsecureCookies !== true) {
2104
+ throw new IQAuthError(
2105
+ "config_invalid",
2106
+ "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."
2107
+ );
2108
+ }
2109
+ const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
2110
+ const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
2111
+ const stateCookieName = config.stateCookieName ?? "iqauth_state";
2112
+ const cookiePath = config.cookiePath ?? "/";
2113
+ const cookieDomain = config.cookieDomain;
2114
+ for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
2115
+ assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
2116
+ }
2044
2117
  return {
2045
2118
  publishableKey: config.publishableKey,
2046
2119
  secretKey: config.secretKey,
2047
2120
  issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
2048
- accessCookieName: config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at",
2049
- refreshCookieName: config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt",
2050
- cookieDomain: config.cookieDomain,
2121
+ accessCookieName,
2122
+ refreshCookieName,
2123
+ cookieDomain,
2051
2124
  sameSite: config.sameSite ?? "lax",
2052
- secure: config.secure ?? true,
2053
- cookiePath: config.cookiePath ?? "/",
2125
+ secure,
2126
+ cookiePath,
2054
2127
  tokenPath: config.tokenPath ?? "/oidc/token",
2055
2128
  refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
2056
2129
  logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
@@ -2063,9 +2136,19 @@ function resolve(config) {
2063
2136
  debug: config.debug,
2064
2137
  onTimingEvent: config.onTimingEvent,
2065
2138
  signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
2066
- signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
2139
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
2140
+ requireOAuthState: config.requireOAuthState ?? true,
2141
+ stateCookieName: config.stateCookieName ?? "iqauth_state"
2067
2142
  };
2068
2143
  }
2144
+ function timingSafeEqualStr(a, b) {
2145
+ const len = Math.max(a.length, b.length);
2146
+ let diff = a.length ^ b.length;
2147
+ for (let i = 0; i < len; i++) {
2148
+ diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
2149
+ }
2150
+ return diff === 0;
2151
+ }
2069
2152
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
2070
2153
  return {
2071
2154
  name,
@@ -2084,6 +2167,9 @@ function clearCookies(cfg) {
2084
2167
  { ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
2085
2168
  ];
2086
2169
  }
2170
+ function clearStateCookie(cfg) {
2171
+ return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
2172
+ }
2087
2173
  var DEFAULT_SIGNOUT_TTL_MS = 6e4;
2088
2174
  var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
2089
2175
  function pruneInMemoryMarkers(now) {
@@ -2109,6 +2195,15 @@ var defaultSignoutRegistry = {
2109
2195
  return true;
2110
2196
  }
2111
2197
  };
2198
+ var warnedDefaultSignoutRegistry = false;
2199
+ function maybeWarnDefaultSignoutRegistry(config) {
2200
+ if (warnedDefaultSignoutRegistry) return;
2201
+ if (config.signoutRegistry) return;
2202
+ warnedDefaultSignoutRegistry = true;
2203
+ console.warn(
2204
+ "[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."
2205
+ );
2206
+ }
2112
2207
  function serializeCookie(d) {
2113
2208
  const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
2114
2209
  parts.push(`Path=${d.path}`);
@@ -2131,6 +2226,23 @@ async function handleCallback(config, input) {
2131
2226
  cookies: []
2132
2227
  };
2133
2228
  }
2229
+ const provided = input.state;
2230
+ const expected = input.expectedState;
2231
+ const stateOk = cfg.requireOAuthState ? !!expected && !!provided && timingSafeEqualStr(provided, expected) : !expected || !!provided && timingSafeEqualStr(provided, expected);
2232
+ if (!stateOk) {
2233
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "STATE_MISMATCH" });
2234
+ return {
2235
+ status: 400,
2236
+ body: {
2237
+ success: false,
2238
+ error: {
2239
+ code: "STATE_MISMATCH",
2240
+ message: "OAuth state validation failed; the sign-in could not be verified as originating from this browser."
2241
+ }
2242
+ },
2243
+ cookies: [clearStateCookie(cfg)]
2244
+ };
2245
+ }
2134
2246
  if (!cfg.secretKey) {
2135
2247
  emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
2136
2248
  return {
@@ -2169,6 +2281,26 @@ async function handleCallback(config, input) {
2169
2281
  cookies: []
2170
2282
  };
2171
2283
  }
2284
+ try {
2285
+ await getTokensFor(cfg.issuer).verify(json.access_token, {
2286
+ issuer: cfg.issuer,
2287
+ ...config.verify
2288
+ });
2289
+ } catch (err) {
2290
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2291
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code });
2292
+ return {
2293
+ status: 502,
2294
+ body: {
2295
+ success: false,
2296
+ error: {
2297
+ code: "ACCESS_TOKEN_VERIFICATION_FAILED",
2298
+ message: "The issuer returned an access token that failed verification; no session was established."
2299
+ }
2300
+ },
2301
+ cookies: []
2302
+ };
2303
+ }
2172
2304
  const cookies = [];
2173
2305
  cookies.push(
2174
2306
  makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
@@ -2176,6 +2308,7 @@ async function handleCallback(config, input) {
2176
2308
  if (json.refresh_token) {
2177
2309
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
2178
2310
  }
2311
+ cookies.push(clearStateCookie(cfg));
2179
2312
  emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
2180
2313
  return {
2181
2314
  status: 200,
@@ -2297,7 +2430,10 @@ async function handleUserinfo(config, input) {
2297
2430
  }
2298
2431
  let claims;
2299
2432
  try {
2300
- claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
2433
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
2434
+ issuer: cfg.issuer,
2435
+ ...config.verify
2436
+ });
2301
2437
  } catch (err) {
2302
2438
  const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2303
2439
  const message = err instanceof Error ? err.message : "Access token verification failed";
@@ -2317,7 +2453,44 @@ async function handleUserinfo(config, input) {
2317
2453
  };
2318
2454
  }
2319
2455
 
2456
+ // src/browser/returnTo.ts
2457
+ function normalizeOrigin(o) {
2458
+ try {
2459
+ return new URL(o).origin;
2460
+ } catch {
2461
+ return o.replace(/\/+$/, "");
2462
+ }
2463
+ }
2464
+ function sanitizeReturnTo(input, options = {}) {
2465
+ const fallback = options.fallback ?? "/";
2466
+ if (!input || typeof input !== "string") return fallback;
2467
+ const trimmed = input.trim();
2468
+ if (!trimmed) return fallback;
2469
+ if (trimmed.includes("\\")) return fallback;
2470
+ if (trimmed.startsWith("//")) return fallback;
2471
+ if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
2472
+ return trimmed;
2473
+ }
2474
+ if (!/^[a-z][a-z0-9+\-.]*:/i.test(trimmed)) {
2475
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
2476
+ }
2477
+ let parsed;
2478
+ try {
2479
+ parsed = new URL(trimmed);
2480
+ } catch {
2481
+ return fallback;
2482
+ }
2483
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return fallback;
2484
+ const currentOrigin = options.currentOrigin ?? (typeof window !== "undefined" ? window.location.origin : "");
2485
+ const allowed = /* @__PURE__ */ new Set();
2486
+ if (currentOrigin) allowed.add(normalizeOrigin(currentOrigin));
2487
+ for (const o of options.allowedOrigins ?? []) allowed.add(normalizeOrigin(o));
2488
+ if (allowed.has(parsed.origin)) return parsed.toString();
2489
+ return fallback;
2490
+ }
2491
+
2320
2492
  // src/fastify.ts
2493
+ var PKCE_COOKIE = "iqauth_pkce";
2321
2494
  var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
2322
2495
  "TOKEN_INVALID",
2323
2496
  "TOKEN_EXPIRED",
@@ -2339,6 +2512,54 @@ function applyResponse(reply, hr) {
2339
2512
  }
2340
2513
  reply.code(hr.status).send(hr.body);
2341
2514
  }
2515
+ function applyCallbackResponse(reply, hr, requestOrigin, returnToCookieValue, returnToCookieName) {
2516
+ const returnTo = sanitizeReturnTo(
2517
+ returnToCookieValue || hr.body?.returnTo,
2518
+ { currentOrigin: requestOrigin, fallback: "/" }
2519
+ );
2520
+ for (const c of hr.cookies) {
2521
+ const cookie = serializeCookie(c);
2522
+ const existing = reply.getHeader?.("set-cookie") ?? [];
2523
+ const list = Array.isArray(existing) ? existing : [existing];
2524
+ list.push(cookie);
2525
+ reply.header("set-cookie", list);
2526
+ }
2527
+ if (hr.status < 400) {
2528
+ const existing = reply.getHeader?.("set-cookie") ?? [];
2529
+ const list = Array.isArray(existing) ? existing : [existing];
2530
+ list.push(`${returnToCookieName}=; Path=/; Max-Age=0; SameSite=Lax`);
2531
+ reply.header("set-cookie", list);
2532
+ }
2533
+ reply.code(hr.status).send({ ...hr.body, returnTo });
2534
+ }
2535
+ function applyCallbackRedirect(reply, hr, requestOrigin, returnToCookieValue, cookieNames) {
2536
+ const pushCookie = (value) => {
2537
+ const existing = reply.getHeader?.("set-cookie") ?? [];
2538
+ const list = Array.isArray(existing) ? existing : [existing];
2539
+ list.push(value);
2540
+ reply.header("set-cookie", list);
2541
+ };
2542
+ for (const c of hr.cookies) pushCookie(serializeCookie(c));
2543
+ pushCookie(`${cookieNames.state}=; Path=/; Max-Age=0; SameSite=Lax`);
2544
+ pushCookie(`${cookieNames.pkce}=; Path=/; Max-Age=0; SameSite=Lax`);
2545
+ if (hr.status >= 400) {
2546
+ reply.header("location", "/");
2547
+ reply.code(302).send();
2548
+ return;
2549
+ }
2550
+ const dest = sanitizeReturnTo(returnToCookieValue, {
2551
+ currentOrigin: requestOrigin,
2552
+ fallback: "/"
2553
+ });
2554
+ pushCookie(`${cookieNames.returnTo}=; Path=/; Max-Age=0; SameSite=Lax`);
2555
+ reply.header("location", dest);
2556
+ reply.code(302).send();
2557
+ }
2558
+ function requestOriginOf(req) {
2559
+ const proto = req.headers?.["x-forwarded-proto"]?.split(",")[0]?.trim() || (typeof req.protocol === "string" ? req.protocol : void 0) || "https";
2560
+ const host = req.headers?.["x-forwarded-host"]?.split(",")[0]?.trim() || req.headers?.host || "";
2561
+ return host ? `${proto}://${host}` : "";
2562
+ }
2342
2563
  function readCookie(req, name) {
2343
2564
  if (req.cookies && typeof req.cookies[name] === "string") return req.cookies[name];
2344
2565
  const raw = req.headers?.cookie;
@@ -2372,6 +2593,7 @@ async function iqAuth(fastify, options) {
2372
2593
  } : void 0;
2373
2594
  const accessCookie = options.accessCookieName ?? "iqauth_at";
2374
2595
  const refreshCookie = options.refreshCookieName ?? "iqauth_rt";
2596
+ const returnToCookie = options.returnToCookieName ?? "iqauth_return_to";
2375
2597
  const mount = (options.mountPath ?? "/api/iqauth").replace(/\/+$/, "");
2376
2598
  const mountHelpers = options.mountHelperRoutes !== false;
2377
2599
  const isPublic = (p) => {
@@ -2406,11 +2628,33 @@ async function iqAuth(fastify, options) {
2406
2628
  if (mountHelpers) {
2407
2629
  fastify.post(`${mount}/callback`, async (req, reply) => {
2408
2630
  const body = req.body || {};
2409
- applyResponse(reply, await handleCallback(helperConfig, {
2631
+ const hr = await handleCallback(helperConfig, {
2410
2632
  code: body.code,
2411
2633
  codeVerifier: body.codeVerifier,
2412
- redirectUri: body.redirectUri
2413
- }));
2634
+ redirectUri: body.redirectUri,
2635
+ // M-2: bind callback to this browser; handleCallback fails closed.
2636
+ state: body.state,
2637
+ expectedState: readCookie(req, helperConfig.stateCookieName ?? "iqauth_state")
2638
+ });
2639
+ applyCallbackResponse(reply, hr, requestOriginOf(req), readCookie(req, returnToCookie), returnToCookie);
2640
+ });
2641
+ fastify.get(`${mount}/callback`, async (req, reply) => {
2642
+ const stateCookie = helperConfig.stateCookieName ?? "iqauth_state";
2643
+ const q = req.query || {};
2644
+ const origin = requestOriginOf(req);
2645
+ const redirectUri = `${origin}${mount}/callback`;
2646
+ const hr = await handleCallback(helperConfig, {
2647
+ code: q.code,
2648
+ codeVerifier: readCookie(req, PKCE_COOKIE),
2649
+ redirectUri,
2650
+ state: q.state,
2651
+ expectedState: readCookie(req, stateCookie)
2652
+ });
2653
+ applyCallbackRedirect(reply, hr, origin, readCookie(req, returnToCookie), {
2654
+ returnTo: returnToCookie,
2655
+ state: stateCookie,
2656
+ pkce: PKCE_COOKIE
2657
+ });
2414
2658
  });
2415
2659
  fastify.post(`${mount}/refresh`, async (req, reply) => {
2416
2660
  const body = req.body || {};
package/dist/fastify.mjs CHANGED
@@ -1,16 +1,19 @@
1
+ import {
2
+ sanitizeReturnTo
3
+ } from "./chunk-JRDVUWAL.mjs";
1
4
  import {
2
5
  handleCallback,
3
6
  handleRefresh,
4
7
  handleSignout,
5
8
  handleUserinfo,
6
9
  serializeCookie
7
- } from "./chunk-RUJXRTEW.mjs";
10
+ } from "./chunk-WSH4SW7F.mjs";
8
11
  import {
9
12
  assertPublishableKey
10
13
  } from "./chunk-HVHNYPDC.mjs";
11
14
  import {
12
15
  IQAuthClient
13
- } from "./chunk-JXQI62A7.mjs";
16
+ } from "./chunk-ZLJPABB7.mjs";
14
17
  import "./chunk-NUO2I65G.mjs";
15
18
  import {
16
19
  IQAuthError
@@ -18,6 +21,7 @@ import {
18
21
  import "./chunk-Y6FXYEAI.mjs";
19
22
 
20
23
  // src/fastify.ts
24
+ var PKCE_COOKIE = "iqauth_pkce";
21
25
  var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
22
26
  "TOKEN_INVALID",
23
27
  "TOKEN_EXPIRED",
@@ -39,6 +43,54 @@ function applyResponse(reply, hr) {
39
43
  }
40
44
  reply.code(hr.status).send(hr.body);
41
45
  }
46
+ function applyCallbackResponse(reply, hr, requestOrigin, returnToCookieValue, returnToCookieName) {
47
+ const returnTo = sanitizeReturnTo(
48
+ returnToCookieValue || hr.body?.returnTo,
49
+ { currentOrigin: requestOrigin, fallback: "/" }
50
+ );
51
+ for (const c of hr.cookies) {
52
+ const cookie = serializeCookie(c);
53
+ const existing = reply.getHeader?.("set-cookie") ?? [];
54
+ const list = Array.isArray(existing) ? existing : [existing];
55
+ list.push(cookie);
56
+ reply.header("set-cookie", list);
57
+ }
58
+ if (hr.status < 400) {
59
+ const existing = reply.getHeader?.("set-cookie") ?? [];
60
+ const list = Array.isArray(existing) ? existing : [existing];
61
+ list.push(`${returnToCookieName}=; Path=/; Max-Age=0; SameSite=Lax`);
62
+ reply.header("set-cookie", list);
63
+ }
64
+ reply.code(hr.status).send({ ...hr.body, returnTo });
65
+ }
66
+ function applyCallbackRedirect(reply, hr, requestOrigin, returnToCookieValue, cookieNames) {
67
+ const pushCookie = (value) => {
68
+ const existing = reply.getHeader?.("set-cookie") ?? [];
69
+ const list = Array.isArray(existing) ? existing : [existing];
70
+ list.push(value);
71
+ reply.header("set-cookie", list);
72
+ };
73
+ for (const c of hr.cookies) pushCookie(serializeCookie(c));
74
+ pushCookie(`${cookieNames.state}=; Path=/; Max-Age=0; SameSite=Lax`);
75
+ pushCookie(`${cookieNames.pkce}=; Path=/; Max-Age=0; SameSite=Lax`);
76
+ if (hr.status >= 400) {
77
+ reply.header("location", "/");
78
+ reply.code(302).send();
79
+ return;
80
+ }
81
+ const dest = sanitizeReturnTo(returnToCookieValue, {
82
+ currentOrigin: requestOrigin,
83
+ fallback: "/"
84
+ });
85
+ pushCookie(`${cookieNames.returnTo}=; Path=/; Max-Age=0; SameSite=Lax`);
86
+ reply.header("location", dest);
87
+ reply.code(302).send();
88
+ }
89
+ function requestOriginOf(req) {
90
+ const proto = req.headers?.["x-forwarded-proto"]?.split(",")[0]?.trim() || (typeof req.protocol === "string" ? req.protocol : void 0) || "https";
91
+ const host = req.headers?.["x-forwarded-host"]?.split(",")[0]?.trim() || req.headers?.host || "";
92
+ return host ? `${proto}://${host}` : "";
93
+ }
42
94
  function readCookie(req, name) {
43
95
  if (req.cookies && typeof req.cookies[name] === "string") return req.cookies[name];
44
96
  const raw = req.headers?.cookie;
@@ -72,6 +124,7 @@ async function iqAuth(fastify, options) {
72
124
  } : void 0;
73
125
  const accessCookie = options.accessCookieName ?? "iqauth_at";
74
126
  const refreshCookie = options.refreshCookieName ?? "iqauth_rt";
127
+ const returnToCookie = options.returnToCookieName ?? "iqauth_return_to";
75
128
  const mount = (options.mountPath ?? "/api/iqauth").replace(/\/+$/, "");
76
129
  const mountHelpers = options.mountHelperRoutes !== false;
77
130
  const isPublic = (p) => {
@@ -106,11 +159,33 @@ async function iqAuth(fastify, options) {
106
159
  if (mountHelpers) {
107
160
  fastify.post(`${mount}/callback`, async (req, reply) => {
108
161
  const body = req.body || {};
109
- applyResponse(reply, await handleCallback(helperConfig, {
162
+ const hr = await handleCallback(helperConfig, {
110
163
  code: body.code,
111
164
  codeVerifier: body.codeVerifier,
112
- redirectUri: body.redirectUri
113
- }));
165
+ redirectUri: body.redirectUri,
166
+ // M-2: bind callback to this browser; handleCallback fails closed.
167
+ state: body.state,
168
+ expectedState: readCookie(req, helperConfig.stateCookieName ?? "iqauth_state")
169
+ });
170
+ applyCallbackResponse(reply, hr, requestOriginOf(req), readCookie(req, returnToCookie), returnToCookie);
171
+ });
172
+ fastify.get(`${mount}/callback`, async (req, reply) => {
173
+ const stateCookie = helperConfig.stateCookieName ?? "iqauth_state";
174
+ const q = req.query || {};
175
+ const origin = requestOriginOf(req);
176
+ const redirectUri = `${origin}${mount}/callback`;
177
+ const hr = await handleCallback(helperConfig, {
178
+ code: q.code,
179
+ codeVerifier: readCookie(req, PKCE_COOKIE),
180
+ redirectUri,
181
+ state: q.state,
182
+ expectedState: readCookie(req, stateCookie)
183
+ });
184
+ applyCallbackRedirect(reply, hr, origin, readCookie(req, returnToCookie), {
185
+ returnTo: returnToCookie,
186
+ state: stateCookie,
187
+ pkce: PKCE_COOKIE
188
+ });
114
189
  });
115
190
  fastify.post(`${mount}/refresh`, async (req, reply) => {
116
191
  const body = req.body || {};
package/dist/hono.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IQAuthHelperConfig } from './server/handlers.mjs';
2
- import './tokens-CITeoG6P.mjs';
3
- import './types-XOV9XPVi.mjs';
2
+ import './tokens-B06VtvUi.mjs';
3
+ import './types-Bn8O-OEd.mjs';
4
4
 
5
5
  /**
6
6
  * @iqauth/sdk/hono — Hono adapter.
@@ -18,6 +18,14 @@ interface IQAuthHonoOptions extends IQAuthHelperConfig {
18
18
  mountPath?: string;
19
19
  mountHelperRoutes?: boolean;
20
20
  publicPaths?: string[] | ((path: string) => boolean);
21
+ /**
22
+ * Cookie name the browser SDK publishes the post-login destination into
23
+ * before redirect. The callback handler reads it, surfaces it as `returnTo`
24
+ * in the JSON response body after a successful exchange, and clears it on
25
+ * success. Mirrors the express inline-callback adapter. Defaults to
26
+ * `iqauth_return_to`.
27
+ */
28
+ returnToCookieName?: string;
21
29
  }
22
30
  declare function iqAuth(options: IQAuthHonoOptions): (c: any, next: () => Promise<void>) => Promise<any>;
23
31
 
package/dist/hono.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IQAuthHelperConfig } from './server/handlers.js';
2
- import './tokens-Bqhmqq_R.js';
3
- import './types-XOV9XPVi.js';
2
+ import './tokens-9F6ETrzk.js';
3
+ import './types-Bn8O-OEd.js';
4
4
 
5
5
  /**
6
6
  * @iqauth/sdk/hono — Hono adapter.
@@ -18,6 +18,14 @@ interface IQAuthHonoOptions extends IQAuthHelperConfig {
18
18
  mountPath?: string;
19
19
  mountHelperRoutes?: boolean;
20
20
  publicPaths?: string[] | ((path: string) => boolean);
21
+ /**
22
+ * Cookie name the browser SDK publishes the post-login destination into
23
+ * before redirect. The callback handler reads it, surfaces it as `returnTo`
24
+ * in the JSON response body after a successful exchange, and clears it on
25
+ * success. Mirrors the express inline-callback adapter. Defaults to
26
+ * `iqauth_return_to`.
27
+ */
28
+ returnToCookieName?: string;
21
29
  }
22
30
  declare function iqAuth(options: IQAuthHonoOptions): (c: any, next: () => Promise<void>) => Promise<any>;
23
31