@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/hono.js CHANGED
@@ -295,17 +295,27 @@ function parseLoginResponse(data, browserSessionMode) {
295
295
  tenants: data.tenants
296
296
  };
297
297
  }
298
+ if (data.type === "scope_selection" && data.scopeSelectionToken && data.scopes && data.tenantId) {
299
+ return {
300
+ status: "scope_selection",
301
+ scopeSelectionToken: data.scopeSelectionToken,
302
+ tenantId: data.tenantId,
303
+ scopes: data.scopes
304
+ };
305
+ }
298
306
  throw new Error("Unexpected login response shape");
299
307
  }
300
308
  var AuthModule = class {
301
309
  constructor(http) {
302
310
  this.http = http;
303
311
  }
304
- async login(email, password) {
312
+ async login(email, password, opts) {
313
+ const body = { email, password };
314
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
305
315
  const data = await this.http.request(
306
316
  "POST",
307
317
  "/api/v1/auth/login",
308
- { email, password },
318
+ body,
309
319
  { skipAutoRefresh: true }
310
320
  );
311
321
  return parseLoginResponse(data, this.http.isBrowserSession());
@@ -343,13 +353,29 @@ var AuthModule = class {
343
353
  method
344
354
  }, { skipAutoRefresh: true });
345
355
  }
346
- async selectTenant(tenantSelectionToken, tenantId) {
356
+ async selectTenant(tenantSelectionToken, tenantId, opts) {
357
+ const body = { tenantSelectionToken, tenantId };
358
+ if (opts?.scopeHint) body.scopeHint = opts.scopeHint;
347
359
  const data = await this.http.request(
348
360
  "POST",
349
361
  "/api/v1/auth/select-tenant",
362
+ body,
363
+ { skipAutoRefresh: true }
364
+ );
365
+ return parseLoginResponse(data, this.http.isBrowserSession());
366
+ }
367
+ /**
368
+ * Task #171 — redeem a scope-selection token + chosen membership for a
369
+ * real authenticated session. `membershipId` must be one of the scopes
370
+ * returned in the prior `scope_selection` envelope.
371
+ */
372
+ async selectScope(scopeSelectionToken, membershipId) {
373
+ const data = await this.http.request(
374
+ "POST",
375
+ "/api/v1/auth/select-scope",
350
376
  {
351
- tenantSelectionToken,
352
- tenantId
377
+ scopeSelectionToken,
378
+ membershipId
353
379
  },
354
380
  { skipAutoRefresh: true }
355
381
  );
@@ -1992,7 +2018,11 @@ async function buildUserinfoResponse(claims, opts = {}) {
1992
2018
  tenantId: claims.tenantId,
1993
2019
  vendorId: claims.vendorId,
1994
2020
  roles: claims.roles ?? [],
1995
- entitlements: claims.entitlements ?? []
2021
+ entitlements: claims.entitlements ?? [],
2022
+ // Task #171 — project the active source/client scope onto the userinfo
2023
+ // payload so server handlers (`getSessionUser`, `/api/iqauth/userinfo`)
2024
+ // expose it without consumers having to re-decode the JWT.
2025
+ ...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
1996
2026
  };
1997
2027
  const enriched = opts.enrich ? await opts.enrich(claims) : null;
1998
2028
  const user = enriched ? { ...baseUser, ...enriched } : baseUser;
@@ -2037,19 +2067,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
2037
2067
  }
2038
2068
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
2039
2069
  var REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
2070
+ function assertCookiePrefixInvariants(name, secure, path, domain) {
2071
+ if (name.startsWith("__Host-")) {
2072
+ if (!secure) {
2073
+ throw new IQAuthError(
2074
+ "config_invalid",
2075
+ `Cookie "${name}" uses the __Host- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
2076
+ );
2077
+ }
2078
+ if (path !== "/") {
2079
+ throw new IQAuthError(
2080
+ "config_invalid",
2081
+ `Cookie "${name}" uses the __Host- prefix, which requires Path=/ (got "${path}"). Remove cookiePath or set it to "/".`
2082
+ );
2083
+ }
2084
+ if (domain) {
2085
+ throw new IQAuthError(
2086
+ "config_invalid",
2087
+ `Cookie "${name}" uses the __Host- prefix, which forbids a Domain attribute (the cookie is host-locked). Remove cookieDomain.`
2088
+ );
2089
+ }
2090
+ } else if (name.startsWith("__Secure-") && !secure) {
2091
+ throw new IQAuthError(
2092
+ "config_invalid",
2093
+ `Cookie "${name}" uses the __Secure- prefix, which browsers only accept on a Secure cookie. Set secure:true (and serve over HTTPS).`
2094
+ );
2095
+ }
2096
+ }
2040
2097
  function resolve(config) {
2041
2098
  const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
2042
2099
  const inferredIssuer = parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`;
2100
+ maybeWarnDefaultSignoutRegistry(config);
2101
+ const secure = config.secure ?? true;
2102
+ if (config.secure === false && config.allowInsecureCookies !== true) {
2103
+ throw new IQAuthError(
2104
+ "config_invalid",
2105
+ "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."
2106
+ );
2107
+ }
2108
+ const accessCookieName = config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at";
2109
+ const refreshCookieName = config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt";
2110
+ const stateCookieName = config.stateCookieName ?? "iqauth_state";
2111
+ const cookiePath = config.cookiePath ?? "/";
2112
+ const cookieDomain = config.cookieDomain;
2113
+ for (const name of [accessCookieName, refreshCookieName, stateCookieName]) {
2114
+ assertCookiePrefixInvariants(name, secure, cookiePath, cookieDomain);
2115
+ }
2043
2116
  return {
2044
2117
  publishableKey: config.publishableKey,
2045
2118
  secretKey: config.secretKey,
2046
2119
  issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
2047
- accessCookieName: config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at",
2048
- refreshCookieName: config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt",
2049
- cookieDomain: config.cookieDomain,
2120
+ accessCookieName,
2121
+ refreshCookieName,
2122
+ cookieDomain,
2050
2123
  sameSite: config.sameSite ?? "lax",
2051
- secure: config.secure ?? true,
2052
- cookiePath: config.cookiePath ?? "/",
2124
+ secure,
2125
+ cookiePath,
2053
2126
  tokenPath: config.tokenPath ?? "/oidc/token",
2054
2127
  refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
2055
2128
  logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
@@ -2062,9 +2135,19 @@ function resolve(config) {
2062
2135
  debug: config.debug,
2063
2136
  onTimingEvent: config.onTimingEvent,
2064
2137
  signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
2065
- signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
2138
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
2139
+ requireOAuthState: config.requireOAuthState ?? true,
2140
+ stateCookieName: config.stateCookieName ?? "iqauth_state"
2066
2141
  };
2067
2142
  }
2143
+ function timingSafeEqualStr(a, b) {
2144
+ const len = Math.max(a.length, b.length);
2145
+ let diff = a.length ^ b.length;
2146
+ for (let i = 0; i < len; i++) {
2147
+ diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
2148
+ }
2149
+ return diff === 0;
2150
+ }
2068
2151
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
2069
2152
  return {
2070
2153
  name,
@@ -2083,6 +2166,9 @@ function clearCookies(cfg) {
2083
2166
  { ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
2084
2167
  ];
2085
2168
  }
2169
+ function clearStateCookie(cfg) {
2170
+ return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
2171
+ }
2086
2172
  var DEFAULT_SIGNOUT_TTL_MS = 6e4;
2087
2173
  var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
2088
2174
  function pruneInMemoryMarkers(now) {
@@ -2108,6 +2194,15 @@ var defaultSignoutRegistry = {
2108
2194
  return true;
2109
2195
  }
2110
2196
  };
2197
+ var warnedDefaultSignoutRegistry = false;
2198
+ function maybeWarnDefaultSignoutRegistry(config) {
2199
+ if (warnedDefaultSignoutRegistry) return;
2200
+ if (config.signoutRegistry) return;
2201
+ warnedDefaultSignoutRegistry = true;
2202
+ console.warn(
2203
+ "[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."
2204
+ );
2205
+ }
2111
2206
  function serializeCookie(d) {
2112
2207
  const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
2113
2208
  parts.push(`Path=${d.path}`);
@@ -2130,6 +2225,23 @@ async function handleCallback(config, input) {
2130
2225
  cookies: []
2131
2226
  };
2132
2227
  }
2228
+ const provided = input.state;
2229
+ const expected = input.expectedState;
2230
+ const stateOk = cfg.requireOAuthState ? !!expected && !!provided && timingSafeEqualStr(provided, expected) : !expected || !!provided && timingSafeEqualStr(provided, expected);
2231
+ if (!stateOk) {
2232
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "STATE_MISMATCH" });
2233
+ return {
2234
+ status: 400,
2235
+ body: {
2236
+ success: false,
2237
+ error: {
2238
+ code: "STATE_MISMATCH",
2239
+ message: "OAuth state validation failed; the sign-in could not be verified as originating from this browser."
2240
+ }
2241
+ },
2242
+ cookies: [clearStateCookie(cfg)]
2243
+ };
2244
+ }
2133
2245
  if (!cfg.secretKey) {
2134
2246
  emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
2135
2247
  return {
@@ -2168,6 +2280,26 @@ async function handleCallback(config, input) {
2168
2280
  cookies: []
2169
2281
  };
2170
2282
  }
2283
+ try {
2284
+ await getTokensFor(cfg.issuer).verify(json.access_token, {
2285
+ issuer: cfg.issuer,
2286
+ ...config.verify
2287
+ });
2288
+ } catch (err) {
2289
+ const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2290
+ emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code });
2291
+ return {
2292
+ status: 502,
2293
+ body: {
2294
+ success: false,
2295
+ error: {
2296
+ code: "ACCESS_TOKEN_VERIFICATION_FAILED",
2297
+ message: "The issuer returned an access token that failed verification; no session was established."
2298
+ }
2299
+ },
2300
+ cookies: []
2301
+ };
2302
+ }
2171
2303
  const cookies = [];
2172
2304
  cookies.push(
2173
2305
  makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
@@ -2175,6 +2307,7 @@ async function handleCallback(config, input) {
2175
2307
  if (json.refresh_token) {
2176
2308
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
2177
2309
  }
2310
+ cookies.push(clearStateCookie(cfg));
2178
2311
  emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
2179
2312
  return {
2180
2313
  status: 200,
@@ -2296,7 +2429,10 @@ async function handleUserinfo(config, input) {
2296
2429
  }
2297
2430
  let claims;
2298
2431
  try {
2299
- claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
2432
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
2433
+ issuer: cfg.issuer,
2434
+ ...config.verify
2435
+ });
2300
2436
  } catch (err) {
2301
2437
  const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
2302
2438
  const message = err instanceof Error ? err.message : "Access token verification failed";
@@ -2316,7 +2452,44 @@ async function handleUserinfo(config, input) {
2316
2452
  };
2317
2453
  }
2318
2454
 
2455
+ // src/browser/returnTo.ts
2456
+ function normalizeOrigin(o) {
2457
+ try {
2458
+ return new URL(o).origin;
2459
+ } catch {
2460
+ return o.replace(/\/+$/, "");
2461
+ }
2462
+ }
2463
+ function sanitizeReturnTo(input, options = {}) {
2464
+ const fallback = options.fallback ?? "/";
2465
+ if (!input || typeof input !== "string") return fallback;
2466
+ const trimmed = input.trim();
2467
+ if (!trimmed) return fallback;
2468
+ if (trimmed.includes("\\")) return fallback;
2469
+ if (trimmed.startsWith("//")) return fallback;
2470
+ if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
2471
+ return trimmed;
2472
+ }
2473
+ if (!/^[a-z][a-z0-9+\-.]*:/i.test(trimmed)) {
2474
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
2475
+ }
2476
+ let parsed;
2477
+ try {
2478
+ parsed = new URL(trimmed);
2479
+ } catch {
2480
+ return fallback;
2481
+ }
2482
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return fallback;
2483
+ const currentOrigin = options.currentOrigin ?? (typeof window !== "undefined" ? window.location.origin : "");
2484
+ const allowed = /* @__PURE__ */ new Set();
2485
+ if (currentOrigin) allowed.add(normalizeOrigin(currentOrigin));
2486
+ for (const o of options.allowedOrigins ?? []) allowed.add(normalizeOrigin(o));
2487
+ if (allowed.has(parsed.origin)) return parsed.toString();
2488
+ return fallback;
2489
+ }
2490
+
2319
2491
  // src/hono.ts
2492
+ var PKCE_COOKIE = "iqauth_pkce";
2320
2493
  var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
2321
2494
  "TOKEN_INVALID",
2322
2495
  "TOKEN_EXPIRED",
@@ -2348,6 +2521,36 @@ function honoResponse(hr) {
2348
2521
  for (const c of hr.cookies) headers.append("set-cookie", serializeCookie(c));
2349
2522
  return new Response(JSON.stringify(hr.body), { status: hr.status, headers });
2350
2523
  }
2524
+ function honoCallbackResponse(hr, requestOrigin, returnToCookieValue, returnToCookieName) {
2525
+ const returnTo = sanitizeReturnTo(
2526
+ returnToCookieValue || hr.body?.returnTo,
2527
+ { currentOrigin: requestOrigin, fallback: "/" }
2528
+ );
2529
+ const headers = new Headers({ "Content-Type": "application/json" });
2530
+ for (const c of hr.cookies) headers.append("set-cookie", serializeCookie(c));
2531
+ if (hr.status < 400) {
2532
+ headers.append("set-cookie", `${returnToCookieName}=; Path=/; Max-Age=0; SameSite=Lax`);
2533
+ }
2534
+ const body = { ...hr.body, returnTo };
2535
+ return new Response(JSON.stringify(body), { status: hr.status, headers });
2536
+ }
2537
+ function honoCallbackRedirect(hr, requestOrigin, returnToCookieValue, cookieNames) {
2538
+ const headers = new Headers();
2539
+ for (const c of hr.cookies) headers.append("set-cookie", serializeCookie(c));
2540
+ headers.append("set-cookie", `${cookieNames.state}=; Path=/; Max-Age=0; SameSite=Lax`);
2541
+ headers.append("set-cookie", `${cookieNames.pkce}=; Path=/; Max-Age=0; SameSite=Lax`);
2542
+ if (hr.status >= 400) {
2543
+ headers.set("location", "/");
2544
+ return new Response(null, { status: 302, headers });
2545
+ }
2546
+ const dest = sanitizeReturnTo(returnToCookieValue, {
2547
+ currentOrigin: requestOrigin,
2548
+ fallback: "/"
2549
+ });
2550
+ headers.append("set-cookie", `${cookieNames.returnTo}=; Path=/; Max-Age=0; SameSite=Lax`);
2551
+ headers.set("location", dest);
2552
+ return new Response(null, { status: 302, headers });
2553
+ }
2351
2554
  function iqAuth(options) {
2352
2555
  const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/hono" });
2353
2556
  const issuer = (options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
@@ -2355,6 +2558,7 @@ function iqAuth(options) {
2355
2558
  const client = new IQAuthClient({ baseUrl: issuer, environment: "server" });
2356
2559
  const accessCookie = options.accessCookieName ?? "iqauth_at";
2357
2560
  const refreshCookie = options.refreshCookieName ?? "iqauth_rt";
2561
+ const returnToCookie = options.returnToCookieName ?? "iqauth_return_to";
2358
2562
  const mount = (options.mountPath ?? "/api/iqauth").replace(/\/+$/, "");
2359
2563
  const mountHelpers = options.mountHelperRoutes !== false;
2360
2564
  const isPublic = (p) => {
@@ -2370,15 +2574,35 @@ function iqAuth(options) {
2370
2574
  const accessToken = auth2 && auth2.replace(/^Bearer /i, "") || readCookieFromHeader(c.req.header("cookie"), accessCookie);
2371
2575
  return honoResponse(await handleUserinfo(helperConfig, { accessToken, req: c.req }));
2372
2576
  }
2577
+ if (mountHelpers && path === `${mount}/callback` && c.req.method === "GET") {
2578
+ const cookieHeader = c.req.header("cookie");
2579
+ const stateCookie = helperConfig.stateCookieName ?? "iqauth_state";
2580
+ const hr = await handleCallback(helperConfig, {
2581
+ code: url.searchParams.get("code") ?? void 0,
2582
+ codeVerifier: readCookieFromHeader(cookieHeader, PKCE_COOKIE),
2583
+ redirectUri: `${url.origin}${url.pathname}`,
2584
+ state: url.searchParams.get("state") ?? void 0,
2585
+ expectedState: readCookieFromHeader(cookieHeader, stateCookie)
2586
+ });
2587
+ return honoCallbackRedirect(hr, url.origin, readCookieFromHeader(cookieHeader, returnToCookie), {
2588
+ returnTo: returnToCookie,
2589
+ state: stateCookie,
2590
+ pkce: PKCE_COOKIE
2591
+ });
2592
+ }
2373
2593
  if (mountHelpers && path.startsWith(mount + "/") && c.req.method === "POST") {
2374
2594
  const body = await c.req.json().catch(() => ({}));
2375
2595
  const cookieHeader = c.req.header("cookie");
2376
2596
  if (path === `${mount}/callback`) {
2377
- return honoResponse(await handleCallback(helperConfig, {
2597
+ const hr = await handleCallback(helperConfig, {
2378
2598
  code: body.code,
2379
2599
  codeVerifier: body.codeVerifier,
2380
- redirectUri: body.redirectUri
2381
- }));
2600
+ redirectUri: body.redirectUri,
2601
+ // M-2: bind callback to this browser; handleCallback fails closed.
2602
+ state: body.state,
2603
+ expectedState: readCookieFromHeader(cookieHeader, helperConfig.stateCookieName ?? "iqauth_state")
2604
+ });
2605
+ return honoCallbackResponse(hr, url.origin, readCookieFromHeader(cookieHeader, returnToCookie), returnToCookie);
2382
2606
  }
2383
2607
  if (path === `${mount}/refresh`) {
2384
2608
  const refreshToken = body.refreshToken || readCookieFromHeader(cookieHeader, refreshCookie);
package/dist/hono.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/hono.ts
24
+ var PKCE_COOKIE = "iqauth_pkce";
21
25
  var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
22
26
  "TOKEN_INVALID",
23
27
  "TOKEN_EXPIRED",
@@ -49,6 +53,36 @@ function honoResponse(hr) {
49
53
  for (const c of hr.cookies) headers.append("set-cookie", serializeCookie(c));
50
54
  return new Response(JSON.stringify(hr.body), { status: hr.status, headers });
51
55
  }
56
+ function honoCallbackResponse(hr, requestOrigin, returnToCookieValue, returnToCookieName) {
57
+ const returnTo = sanitizeReturnTo(
58
+ returnToCookieValue || hr.body?.returnTo,
59
+ { currentOrigin: requestOrigin, fallback: "/" }
60
+ );
61
+ const headers = new Headers({ "Content-Type": "application/json" });
62
+ for (const c of hr.cookies) headers.append("set-cookie", serializeCookie(c));
63
+ if (hr.status < 400) {
64
+ headers.append("set-cookie", `${returnToCookieName}=; Path=/; Max-Age=0; SameSite=Lax`);
65
+ }
66
+ const body = { ...hr.body, returnTo };
67
+ return new Response(JSON.stringify(body), { status: hr.status, headers });
68
+ }
69
+ function honoCallbackRedirect(hr, requestOrigin, returnToCookieValue, cookieNames) {
70
+ const headers = new Headers();
71
+ for (const c of hr.cookies) headers.append("set-cookie", serializeCookie(c));
72
+ headers.append("set-cookie", `${cookieNames.state}=; Path=/; Max-Age=0; SameSite=Lax`);
73
+ headers.append("set-cookie", `${cookieNames.pkce}=; Path=/; Max-Age=0; SameSite=Lax`);
74
+ if (hr.status >= 400) {
75
+ headers.set("location", "/");
76
+ return new Response(null, { status: 302, headers });
77
+ }
78
+ const dest = sanitizeReturnTo(returnToCookieValue, {
79
+ currentOrigin: requestOrigin,
80
+ fallback: "/"
81
+ });
82
+ headers.append("set-cookie", `${cookieNames.returnTo}=; Path=/; Max-Age=0; SameSite=Lax`);
83
+ headers.set("location", dest);
84
+ return new Response(null, { status: 302, headers });
85
+ }
52
86
  function iqAuth(options) {
53
87
  const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/hono" });
54
88
  const issuer = (options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`)).replace(/\/+$/, "");
@@ -56,6 +90,7 @@ function iqAuth(options) {
56
90
  const client = new IQAuthClient({ baseUrl: issuer, environment: "server" });
57
91
  const accessCookie = options.accessCookieName ?? "iqauth_at";
58
92
  const refreshCookie = options.refreshCookieName ?? "iqauth_rt";
93
+ const returnToCookie = options.returnToCookieName ?? "iqauth_return_to";
59
94
  const mount = (options.mountPath ?? "/api/iqauth").replace(/\/+$/, "");
60
95
  const mountHelpers = options.mountHelperRoutes !== false;
61
96
  const isPublic = (p) => {
@@ -71,15 +106,35 @@ function iqAuth(options) {
71
106
  const accessToken = auth2 && auth2.replace(/^Bearer /i, "") || readCookieFromHeader(c.req.header("cookie"), accessCookie);
72
107
  return honoResponse(await handleUserinfo(helperConfig, { accessToken, req: c.req }));
73
108
  }
109
+ if (mountHelpers && path === `${mount}/callback` && c.req.method === "GET") {
110
+ const cookieHeader = c.req.header("cookie");
111
+ const stateCookie = helperConfig.stateCookieName ?? "iqauth_state";
112
+ const hr = await handleCallback(helperConfig, {
113
+ code: url.searchParams.get("code") ?? void 0,
114
+ codeVerifier: readCookieFromHeader(cookieHeader, PKCE_COOKIE),
115
+ redirectUri: `${url.origin}${url.pathname}`,
116
+ state: url.searchParams.get("state") ?? void 0,
117
+ expectedState: readCookieFromHeader(cookieHeader, stateCookie)
118
+ });
119
+ return honoCallbackRedirect(hr, url.origin, readCookieFromHeader(cookieHeader, returnToCookie), {
120
+ returnTo: returnToCookie,
121
+ state: stateCookie,
122
+ pkce: PKCE_COOKIE
123
+ });
124
+ }
74
125
  if (mountHelpers && path.startsWith(mount + "/") && c.req.method === "POST") {
75
126
  const body = await c.req.json().catch(() => ({}));
76
127
  const cookieHeader = c.req.header("cookie");
77
128
  if (path === `${mount}/callback`) {
78
- return honoResponse(await handleCallback(helperConfig, {
129
+ const hr = await handleCallback(helperConfig, {
79
130
  code: body.code,
80
131
  codeVerifier: body.codeVerifier,
81
- redirectUri: body.redirectUri
82
- }));
132
+ redirectUri: body.redirectUri,
133
+ // M-2: bind callback to this browser; handleCallback fails closed.
134
+ state: body.state,
135
+ expectedState: readCookieFromHeader(cookieHeader, helperConfig.stateCookieName ?? "iqauth_state")
136
+ });
137
+ return honoCallbackResponse(hr, url.origin, readCookieFromHeader(cookieHeader, returnToCookie), returnToCookie);
83
138
  }
84
139
  if (path === `${mount}/refresh`) {
85
140
  const refreshToken = body.refreshToken || readCookieFromHeader(cookieHeader, refreshCookie);