@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/react.mjs CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  Protect,
17
17
  RedirectToSignIn,
18
18
  RedirectToSignedIn,
19
+ ScopeSwitcher,
19
20
  SignIn,
20
21
  SignUp,
21
22
  SignedIn,
@@ -25,12 +26,14 @@ import {
25
26
  Waitlist,
26
27
  __useIQAuthInternal,
27
28
  __version__,
28
- isReturnToAllowed,
29
+ claimSatisfiesScope,
29
30
  isSilentSsoEligible,
31
+ performScopeSwitch,
32
+ performTenantSwitch,
30
33
  preflightReturnTo,
34
+ resolveAfterSignInDestination,
31
35
  revokeSession,
32
36
  sanitizeBrandCss,
33
- sanitizeReturnTo,
34
37
  slugify,
35
38
  useAccountList,
36
39
  useAccountSwitcher,
@@ -41,6 +44,7 @@ import {
41
44
  useLinkedIdentities,
42
45
  useLocale,
43
46
  useMagicLink,
47
+ useMemberships,
44
48
  useOrganization,
45
49
  usePasskey,
46
50
  useResolvedSdkBranding,
@@ -50,12 +54,16 @@ import {
50
54
  useSessionList,
51
55
  useT,
52
56
  useUser
53
- } from "./chunk-RR2MGPTK.mjs";
54
- import "./chunk-RTJAIBXY.mjs";
57
+ } from "./chunk-WHT6WKTY.mjs";
58
+ import "./chunk-4V7FKOTG.mjs";
55
59
  import "./chunk-GN37E64I.mjs";
56
60
  import "./chunk-C2ZTBOAC.mjs";
61
+ import {
62
+ isReturnToAllowed,
63
+ sanitizeReturnTo
64
+ } from "./chunk-JRDVUWAL.mjs";
57
65
  import "./chunk-HVHNYPDC.mjs";
58
- import "./chunk-5T7GHBX6.mjs";
66
+ import "./chunk-TLET552H.mjs";
59
67
  import "./chunk-6PJRLRB4.mjs";
60
68
  import "./chunk-Y6FXYEAI.mjs";
61
69
  export {
@@ -76,6 +84,7 @@ export {
76
84
  Protect,
77
85
  RedirectToSignIn,
78
86
  RedirectToSignedIn,
87
+ ScopeSwitcher,
79
88
  SignIn,
80
89
  SignUp,
81
90
  SignedIn,
@@ -85,9 +94,13 @@ export {
85
94
  Waitlist,
86
95
  __useIQAuthInternal,
87
96
  __version__,
97
+ claimSatisfiesScope,
88
98
  isReturnToAllowed,
89
99
  isSilentSsoEligible,
100
+ performScopeSwitch,
101
+ performTenantSwitch,
90
102
  preflightReturnTo,
103
+ resolveAfterSignInDestination,
91
104
  revokeSession,
92
105
  sanitizeBrandCss,
93
106
  sanitizeReturnTo,
@@ -101,6 +114,7 @@ export {
101
114
  useLinkedIdentities,
102
115
  useLocale,
103
116
  useMagicLink,
117
+ useMemberships,
104
118
  useOrganization,
105
119
  usePasskey,
106
120
  useResolvedSdkBranding,
@@ -1,5 +1,5 @@
1
- import { c as TokenVerifyOptions } from '../tokens-CITeoG6P.mjs';
2
- import { J as JwtClaims, S as SessionUser } from '../types-XOV9XPVi.mjs';
1
+ import { c as TokenVerifyOptions } from '../tokens-B06VtvUi.mjs';
2
+ import { J as JwtClaims, S as SessionUser } from '../types-Bn8O-OEd.mjs';
3
3
 
4
4
  /**
5
5
  * Framework-neutral helper handlers for the auto-mounted routes added by the
@@ -99,8 +99,21 @@ interface IQAuthHelperConfig {
99
99
  cookieDomain?: string;
100
100
  /** Cookie sameSite policy. Defaults to `lax`. */
101
101
  sameSite?: "lax" | "strict" | "none";
102
- /** Cookie secure flag. Defaults to true (set false for local http dev). */
102
+ /**
103
+ * Cookie secure flag. Defaults to `true`. Setting it to `false` ships auth
104
+ * cookies over plaintext HTTP, exposing them to passive network attackers —
105
+ * so it is REFUSED by default (the helper throws `config_invalid`). To run a
106
+ * local HTTP dev box you must explicitly acknowledge the risk by ALSO setting
107
+ * {@link IQAuthHelperConfig.allowInsecureCookies} to `true`.
108
+ */
103
109
  secure?: boolean;
110
+ /**
111
+ * Explicit, opt-in acknowledgement that `secure: false` is intentional (local
112
+ * HTTP development only). Without this, `secure: false` throws so a misconfig
113
+ * can never silently downgrade production cookies to plaintext. Has no effect
114
+ * unless `secure` is `false`. Production MUST leave this unset / `false`.
115
+ */
116
+ allowInsecureCookies?: boolean;
104
117
  /** Cookie path. Defaults to `/`. */
105
118
  cookiePath?: string;
106
119
  /** Path of the OIDC token endpoint. */
@@ -183,6 +196,30 @@ interface IQAuthHelperConfig {
183
196
  * legitimate fresh sign-in.
184
197
  */
185
198
  signoutMarkerTtlMs?: number;
199
+ /**
200
+ * M-2 — Server-side OAuth `state` (CSRF) enforcement on the callback.
201
+ *
202
+ * When `true` (the DEFAULT), {@link handleCallback} requires BOTH the
203
+ * `state` returned by the OAuth redirect AND a previously-stored
204
+ * `expectedState` (read by the adapter from the {@link stateCookieName}
205
+ * cookie the SDK publishes before redirect), and fails closed with
206
+ * `STATE_MISMATCH` (no code exchange, no session cookie) when either is
207
+ * missing or they do not match. This defeats login-CSRF / session-fixation
208
+ * where an attacker injects their own authorization code.
209
+ *
210
+ * Set to `false` to restore the prior permissive behavior (state is only
211
+ * validated when an `expectedState` cookie happens to be present). This is
212
+ * an escape hatch for integrators whose initiation step does not publish a
213
+ * state cookie; it weakens CSRF protection and is not recommended.
214
+ */
215
+ requireOAuthState?: boolean;
216
+ /**
217
+ * Name of the first-party cookie the SDK publishes the OAuth `state` value
218
+ * into before redirecting to the issuer. The adapter reads it back on the
219
+ * callback as `expectedState`. Defaults to `iqauth_state` (matches the
220
+ * express inline-callback adapter and the React server-managed sign-in).
221
+ */
222
+ stateCookieName?: string;
186
223
  }
187
224
  interface IQAuthTimingEvent {
188
225
  phase: "callback" | "refresh" | "signout" | "bootstrap" | "signIn";
@@ -202,7 +239,7 @@ interface SignoutRegistry {
202
239
  mark(idempotencyToken: string, ttlMs: number): void | Promise<void>;
203
240
  has(idempotencyToken: string): boolean | Promise<boolean>;
204
241
  }
205
- interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure" | "cookieNames" | "mountUserinfo" | "userinfoEnricher" | "verify" | "debug" | "onTimingEvent" | "signoutRegistry" | "signoutMarkerTtlMs">> {
242
+ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure" | "cookieNames" | "mountUserinfo" | "userinfoEnricher" | "verify" | "debug" | "onTimingEvent" | "signoutRegistry" | "signoutMarkerTtlMs" | "allowInsecureCookies">> {
206
243
  cookieNames?: {
207
244
  access?: string;
208
245
  refresh?: string;
@@ -225,6 +262,12 @@ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" |
225
262
  * public surface.
226
263
  */
227
264
  declare function __resetSignoutMarkersForTests(): void;
265
+ /**
266
+ * Test-only hook to reset the one-time default-registry boot warning so each
267
+ * test can assert the warn-once behavior in isolation. Not part of the public
268
+ * surface.
269
+ */
270
+ declare function __resetSignoutRegistryWarningForTests(): void;
228
271
  /**
229
272
  * Public factory for a fresh in-memory {@link SignoutRegistry}. Useful for
230
273
  * tests that want isolated state per case, and as a reference implementation
@@ -242,6 +285,14 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
242
285
  code?: string;
243
286
  codeVerifier?: string;
244
287
  redirectUri?: string;
288
+ /** The `state` value returned by the OAuth redirect (query or body). */
289
+ state?: string;
290
+ /**
291
+ * The `state` value the SDK stored before redirect, read by the adapter
292
+ * from the {@link IQAuthHelperConfig.stateCookieName} cookie. Compared
293
+ * against `state` to bind the callback to this browser (M-2 CSRF).
294
+ */
295
+ expectedState?: string;
245
296
  }): Promise<HandlerResponse>;
246
297
  /** POST /api/iqauth/refresh — rotate refresh + access cookies.
247
298
  *
@@ -276,4 +327,4 @@ declare function handleUserinfo(config: IQAuthHelperConfig, input: {
276
327
  req?: unknown;
277
328
  }): Promise<HandlerResponse>;
278
329
 
279
- export { type HandlerResponse, type IQAuthHelperConfig, type IQAuthTimingEvent, type ResolvedConfig as ResolvedIQAuthHelperConfig, type SetCookieDirective, type SignoutRegistry, type UserinfoResponse, __resetSignoutMarkersForTests, buildUserinfoResponse, createInMemorySignoutRegistry, handleCallback, handleRefresh, handleSignout, handleUserinfo, serializeCookie };
330
+ export { type HandlerResponse, type IQAuthHelperConfig, type IQAuthTimingEvent, type ResolvedConfig as ResolvedIQAuthHelperConfig, type SetCookieDirective, type SignoutRegistry, type UserinfoResponse, __resetSignoutMarkersForTests, __resetSignoutRegistryWarningForTests, buildUserinfoResponse, createInMemorySignoutRegistry, handleCallback, handleRefresh, handleSignout, handleUserinfo, serializeCookie };
@@ -1,5 +1,5 @@
1
- import { c as TokenVerifyOptions } from '../tokens-Bqhmqq_R.js';
2
- import { J as JwtClaims, S as SessionUser } from '../types-XOV9XPVi.js';
1
+ import { c as TokenVerifyOptions } from '../tokens-9F6ETrzk.js';
2
+ import { J as JwtClaims, S as SessionUser } from '../types-Bn8O-OEd.js';
3
3
 
4
4
  /**
5
5
  * Framework-neutral helper handlers for the auto-mounted routes added by the
@@ -99,8 +99,21 @@ interface IQAuthHelperConfig {
99
99
  cookieDomain?: string;
100
100
  /** Cookie sameSite policy. Defaults to `lax`. */
101
101
  sameSite?: "lax" | "strict" | "none";
102
- /** Cookie secure flag. Defaults to true (set false for local http dev). */
102
+ /**
103
+ * Cookie secure flag. Defaults to `true`. Setting it to `false` ships auth
104
+ * cookies over plaintext HTTP, exposing them to passive network attackers —
105
+ * so it is REFUSED by default (the helper throws `config_invalid`). To run a
106
+ * local HTTP dev box you must explicitly acknowledge the risk by ALSO setting
107
+ * {@link IQAuthHelperConfig.allowInsecureCookies} to `true`.
108
+ */
103
109
  secure?: boolean;
110
+ /**
111
+ * Explicit, opt-in acknowledgement that `secure: false` is intentional (local
112
+ * HTTP development only). Without this, `secure: false` throws so a misconfig
113
+ * can never silently downgrade production cookies to plaintext. Has no effect
114
+ * unless `secure` is `false`. Production MUST leave this unset / `false`.
115
+ */
116
+ allowInsecureCookies?: boolean;
104
117
  /** Cookie path. Defaults to `/`. */
105
118
  cookiePath?: string;
106
119
  /** Path of the OIDC token endpoint. */
@@ -183,6 +196,30 @@ interface IQAuthHelperConfig {
183
196
  * legitimate fresh sign-in.
184
197
  */
185
198
  signoutMarkerTtlMs?: number;
199
+ /**
200
+ * M-2 — Server-side OAuth `state` (CSRF) enforcement on the callback.
201
+ *
202
+ * When `true` (the DEFAULT), {@link handleCallback} requires BOTH the
203
+ * `state` returned by the OAuth redirect AND a previously-stored
204
+ * `expectedState` (read by the adapter from the {@link stateCookieName}
205
+ * cookie the SDK publishes before redirect), and fails closed with
206
+ * `STATE_MISMATCH` (no code exchange, no session cookie) when either is
207
+ * missing or they do not match. This defeats login-CSRF / session-fixation
208
+ * where an attacker injects their own authorization code.
209
+ *
210
+ * Set to `false` to restore the prior permissive behavior (state is only
211
+ * validated when an `expectedState` cookie happens to be present). This is
212
+ * an escape hatch for integrators whose initiation step does not publish a
213
+ * state cookie; it weakens CSRF protection and is not recommended.
214
+ */
215
+ requireOAuthState?: boolean;
216
+ /**
217
+ * Name of the first-party cookie the SDK publishes the OAuth `state` value
218
+ * into before redirecting to the issuer. The adapter reads it back on the
219
+ * callback as `expectedState`. Defaults to `iqauth_state` (matches the
220
+ * express inline-callback adapter and the React server-managed sign-in).
221
+ */
222
+ stateCookieName?: string;
186
223
  }
187
224
  interface IQAuthTimingEvent {
188
225
  phase: "callback" | "refresh" | "signout" | "bootstrap" | "signIn";
@@ -202,7 +239,7 @@ interface SignoutRegistry {
202
239
  mark(idempotencyToken: string, ttlMs: number): void | Promise<void>;
203
240
  has(idempotencyToken: string): boolean | Promise<boolean>;
204
241
  }
205
- interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure" | "cookieNames" | "mountUserinfo" | "userinfoEnricher" | "verify" | "debug" | "onTimingEvent" | "signoutRegistry" | "signoutMarkerTtlMs">> {
242
+ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure" | "cookieNames" | "mountUserinfo" | "userinfoEnricher" | "verify" | "debug" | "onTimingEvent" | "signoutRegistry" | "signoutMarkerTtlMs" | "allowInsecureCookies">> {
206
243
  cookieNames?: {
207
244
  access?: string;
208
245
  refresh?: string;
@@ -225,6 +262,12 @@ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" |
225
262
  * public surface.
226
263
  */
227
264
  declare function __resetSignoutMarkersForTests(): void;
265
+ /**
266
+ * Test-only hook to reset the one-time default-registry boot warning so each
267
+ * test can assert the warn-once behavior in isolation. Not part of the public
268
+ * surface.
269
+ */
270
+ declare function __resetSignoutRegistryWarningForTests(): void;
228
271
  /**
229
272
  * Public factory for a fresh in-memory {@link SignoutRegistry}. Useful for
230
273
  * tests that want isolated state per case, and as a reference implementation
@@ -242,6 +285,14 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
242
285
  code?: string;
243
286
  codeVerifier?: string;
244
287
  redirectUri?: string;
288
+ /** The `state` value returned by the OAuth redirect (query or body). */
289
+ state?: string;
290
+ /**
291
+ * The `state` value the SDK stored before redirect, read by the adapter
292
+ * from the {@link IQAuthHelperConfig.stateCookieName} cookie. Compared
293
+ * against `state` to bind the callback to this browser (M-2 CSRF).
294
+ */
295
+ expectedState?: string;
245
296
  }): Promise<HandlerResponse>;
246
297
  /** POST /api/iqauth/refresh — rotate refresh + access cookies.
247
298
  *
@@ -276,4 +327,4 @@ declare function handleUserinfo(config: IQAuthHelperConfig, input: {
276
327
  req?: unknown;
277
328
  }): Promise<HandlerResponse>;
278
329
 
279
- export { type HandlerResponse, type IQAuthHelperConfig, type IQAuthTimingEvent, type ResolvedConfig as ResolvedIQAuthHelperConfig, type SetCookieDirective, type SignoutRegistry, type UserinfoResponse, __resetSignoutMarkersForTests, buildUserinfoResponse, createInMemorySignoutRegistry, handleCallback, handleRefresh, handleSignout, handleUserinfo, serializeCookie };
330
+ export { type HandlerResponse, type IQAuthHelperConfig, type IQAuthTimingEvent, type ResolvedConfig as ResolvedIQAuthHelperConfig, type SetCookieDirective, type SignoutRegistry, type UserinfoResponse, __resetSignoutMarkersForTests, __resetSignoutRegistryWarningForTests, buildUserinfoResponse, createInMemorySignoutRegistry, handleCallback, handleRefresh, handleSignout, handleUserinfo, serializeCookie };
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var handlers_exports = {};
22
22
  __export(handlers_exports, {
23
23
  __resetSignoutMarkersForTests: () => __resetSignoutMarkersForTests,
24
+ __resetSignoutRegistryWarningForTests: () => __resetSignoutRegistryWarningForTests,
24
25
  buildUserinfoResponse: () => buildUserinfoResponse,
25
26
  createInMemorySignoutRegistry: () => createInMemorySignoutRegistry,
26
27
  handleCallback: () => handleCallback,
@@ -362,7 +363,11 @@ async function buildUserinfoResponse(claims, opts = {}) {
362
363
  tenantId: claims.tenantId,
363
364
  vendorId: claims.vendorId,
364
365
  roles: claims.roles ?? [],
365
- entitlements: claims.entitlements ?? []
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 } : {}
366
371
  };
367
372
  const enriched = opts.enrich ? await opts.enrich(claims) : null;
368
373
  const user = enriched ? { ...baseUser, ...enriched } : baseUser;
@@ -407,19 +412,62 @@ function shouldClearCookiesOnFailure(policy, status, errorCode) {
407
412
  }
408
413
  var ACCESS_TOKEN_TTL_SECONDS = 60 * 15;
409
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
+ }
410
442
  function resolve(config) {
411
443
  const parsed = assertPublishableKey(config.publishableKey, { context: "@iqauth/sdk helpers" });
412
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
+ }
413
461
  return {
414
462
  publishableKey: config.publishableKey,
415
463
  secretKey: config.secretKey,
416
464
  issuer: (config.issuer ?? inferredIssuer).replace(/\/+$/, ""),
417
- accessCookieName: config.accessCookieName ?? config.cookieNames?.access ?? "iqauth_at",
418
- refreshCookieName: config.refreshCookieName ?? config.cookieNames?.refresh ?? "iqauth_rt",
419
- cookieDomain: config.cookieDomain,
465
+ accessCookieName,
466
+ refreshCookieName,
467
+ cookieDomain,
420
468
  sameSite: config.sameSite ?? "lax",
421
- secure: config.secure ?? true,
422
- cookiePath: config.cookiePath ?? "/",
469
+ secure,
470
+ cookiePath,
423
471
  tokenPath: config.tokenPath ?? "/oidc/token",
424
472
  refreshPath: config.refreshPath ?? "/api/v1/auth/refresh",
425
473
  logoutPath: config.logoutPath ?? "/api/v1/auth/logout",
@@ -432,9 +480,19 @@ function resolve(config) {
432
480
  debug: config.debug,
433
481
  onTimingEvent: config.onTimingEvent,
434
482
  signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
435
- signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
483
+ signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS,
484
+ requireOAuthState: config.requireOAuthState ?? true,
485
+ stateCookieName: config.stateCookieName ?? "iqauth_state"
436
486
  };
437
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
+ }
438
496
  function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
439
497
  return {
440
498
  name,
@@ -453,6 +511,9 @@ function clearCookies(cfg) {
453
511
  { ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
454
512
  ];
455
513
  }
514
+ function clearStateCookie(cfg) {
515
+ return { ...makeCookie(cfg, cfg.stateCookieName, "", 0, false), clear: true };
516
+ }
456
517
  var DEFAULT_SIGNOUT_TTL_MS = 6e4;
457
518
  var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
458
519
  function pruneInMemoryMarkers(now) {
@@ -478,9 +539,21 @@ var defaultSignoutRegistry = {
478
539
  return true;
479
540
  }
480
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
+ }
481
551
  function __resetSignoutMarkersForTests() {
482
552
  inMemorySignoutMarkers.clear();
483
553
  }
554
+ function __resetSignoutRegistryWarningForTests() {
555
+ warnedDefaultSignoutRegistry = false;
556
+ }
484
557
  function createInMemorySignoutRegistry() {
485
558
  const store = /* @__PURE__ */ new Map();
486
559
  return {
@@ -523,6 +596,23 @@ async function handleCallback(config, input) {
523
596
  cookies: []
524
597
  };
525
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
+ }
526
616
  if (!cfg.secretKey) {
527
617
  emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
528
618
  return {
@@ -561,6 +651,26 @@ async function handleCallback(config, input) {
561
651
  cookies: []
562
652
  };
563
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
+ }
564
674
  const cookies = [];
565
675
  cookies.push(
566
676
  makeCookie(cfg, cfg.accessCookieName, json.access_token, json.expires_in ?? ACCESS_TOKEN_TTL_SECONDS)
@@ -568,6 +678,7 @@ async function handleCallback(config, input) {
568
678
  if (json.refresh_token) {
569
679
  cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
570
680
  }
681
+ cookies.push(clearStateCookie(cfg));
571
682
  emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
572
683
  return {
573
684
  status: 200,
@@ -689,7 +800,10 @@ async function handleUserinfo(config, input) {
689
800
  }
690
801
  let claims;
691
802
  try {
692
- claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
803
+ claims = await getTokensFor(cfg.issuer).verify(input.accessToken, {
804
+ issuer: cfg.issuer,
805
+ ...config.verify
806
+ });
693
807
  } catch (err) {
694
808
  const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
695
809
  const message = err instanceof Error ? err.message : "Access token verification failed";
@@ -711,6 +825,7 @@ async function handleUserinfo(config, input) {
711
825
  // Annotate the CommonJS export names for ESM import in node:
712
826
  0 && (module.exports = {
713
827
  __resetSignoutMarkersForTests,
828
+ __resetSignoutRegistryWarningForTests,
714
829
  buildUserinfoResponse,
715
830
  createInMemorySignoutRegistry,
716
831
  handleCallback,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  __resetSignoutMarkersForTests,
3
+ __resetSignoutRegistryWarningForTests,
3
4
  buildUserinfoResponse,
4
5
  createInMemorySignoutRegistry,
5
6
  handleCallback,
@@ -7,13 +8,14 @@ import {
7
8
  handleSignout,
8
9
  handleUserinfo,
9
10
  serializeCookie
10
- } from "../chunk-RUJXRTEW.mjs";
11
+ } from "../chunk-WSH4SW7F.mjs";
11
12
  import "../chunk-HVHNYPDC.mjs";
12
13
  import "../chunk-NUO2I65G.mjs";
13
14
  import "../chunk-6PJRLRB4.mjs";
14
15
  import "../chunk-Y6FXYEAI.mjs";
15
16
  export {
16
17
  __resetSignoutMarkersForTests,
18
+ __resetSignoutRegistryWarningForTests,
17
19
  buildUserinfoResponse,
18
20
  createInMemorySignoutRegistry,
19
21
  handleCallback,
package/dist/server.d.mts CHANGED
@@ -1,10 +1,10 @@
1
- import { J as JwtClaims, f as IQAuthTokenClientConfig, X as ExpressMiddlewareOptions, a as IQAuthRequestLike, b as IQAuthResponseLike, c as IQAuthNextFunction } from './types-XOV9XPVi.mjs';
2
- import { I as IQAuthClient } from './client-BGFnBpfc.mjs';
1
+ import { J as JwtClaims, f as IQAuthTokenClientConfig, X as ExpressMiddlewareOptions, a as IQAuthRequestLike, b as IQAuthResponseLike, c as IQAuthNextFunction } from './types-Bn8O-OEd.mjs';
2
+ import { I as IQAuthClient } from './client-D8L-PaWr.mjs';
3
3
  export { E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.mjs';
4
- export { C as CookieAwareMiddlewareOptions, D as DEFAULT_ACCESS_COOKIE, a as DEFAULT_REFRESH_COOKIE, i as iqAuthMiddleware } from './express-CVNQEkOr.mjs';
4
+ export { C as CookieAwareMiddlewareOptions, D as DEFAULT_ACCESS_COOKIE, a as DEFAULT_REFRESH_COOKIE, i as iqAuthMiddleware } from './express-DDTA3qV1.mjs';
5
5
  export { HandlerResponse, IQAuthHelperConfig, SetCookieDirective, UserinfoResponse, buildUserinfoResponse, handleCallback, handleRefresh, handleSignout, handleUserinfo, serializeCookie } from './server/handlers.mjs';
6
- export { P as ProvisioningBridge, a as ProvisioningBridgeOptions, d as ProvisioningContext, b as ProvisioningStorage, c as createProvisioningBridge } from './provisioningBridge-M5G47LWO.mjs';
7
- import './tokens-CITeoG6P.mjs';
6
+ export { P as ProvisioningBridge, a as ProvisioningBridgeOptions, d as ProvisioningContext, e as ProvisioningError, b as ProvisioningStorage, c as createProvisioningBridge } from './provisioningBridge-IEycmsgb.mjs';
7
+ import './tokens-B06VtvUi.mjs';
8
8
 
9
9
  /**
10
10
  * linkLocalUserToIqAuthSub — first-time-migration helper.
@@ -31,9 +31,17 @@ import './tokens-CITeoG6P.mjs';
31
31
  * - 'linked' — wrote claims.sub onto the matched local row.
32
32
  * - 'already_linked' — a row was already keyed by claims.sub. No write.
33
33
  * - 'conflict' — matched row already has a different non-null sub,
34
- * OR more than one local row matches the email.
34
+ * OR more than one local row matches the email,
35
+ * OR the email is unverified and adoption is gated
36
+ * (reason 'unverified_email' — see H-4 below).
35
37
  * - 'not_found' — no local row matched any of the provided lookupBy
36
38
  * keys. Caller should provision a new row separately.
39
+ *
40
+ * Security (H-4): writing the IQAuth `sub` onto a pre-existing local row matched
41
+ * by email is a takeover of that account. It is only performed when the claims
42
+ * assert a verified email (`claims.email_verified === true`). When the email is
43
+ * unverified and `allowUnverifiedEmail` is not set, the helper fails closed with
44
+ * `{ status: 'conflict', reason: 'unverified_email' }` instead of linking.
37
45
  */
38
46
 
39
47
  type LinkLookupBy = "email";
@@ -46,7 +54,7 @@ type LinkResult = {
46
54
  } | {
47
55
  status: "conflict";
48
56
  userId?: string;
49
- reason: "different_sub" | "duplicate_email";
57
+ reason: "different_sub" | "duplicate_email" | "unverified_email";
50
58
  } | {
51
59
  status: "not_found";
52
60
  };
@@ -85,7 +93,7 @@ interface LinkAdapter {
85
93
  }
86
94
  interface LinkLocalUserOptions {
87
95
  adapter: LinkAdapter;
88
- claims: Pick<JwtClaims, "sub" | "email">;
96
+ claims: Pick<JwtClaims, "sub" | "email" | "email_verified">;
89
97
  /**
90
98
  * Lookup keys to try, in order. Currently only `'email'` is supported.
91
99
  * Defaults to `['email']`.
@@ -96,6 +104,18 @@ interface LinkLocalUserOptions {
96
104
  * but preserve the cased original at registration time.
97
105
  */
98
106
  caseInsensitiveEmail?: boolean;
107
+ /**
108
+ * Security gate (H-4): by default the email→sub link only proceeds when the
109
+ * claims assert a verified email (`claims.email_verified === true`). When the
110
+ * email is unverified and this flag is left `false`, the helper fails closed
111
+ * with `{ status: 'conflict', reason: 'unverified_email' }` rather than
112
+ * letting an unverified email take over a pre-existing local account.
113
+ *
114
+ * Set `true` ONLY when your issuer is trusted to never emit an unverified
115
+ * email for linking (or you have a compensating control). Defaults to
116
+ * `false` (secure).
117
+ */
118
+ allowUnverifiedEmail?: boolean;
99
119
  }
100
120
  declare function linkLocalUserToIqAuthSub(options: LinkLocalUserOptions): Promise<LinkResult>;
101
121
  interface DrizzleLikeDb {
package/dist/server.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { J as JwtClaims, f as IQAuthTokenClientConfig, X as ExpressMiddlewareOptions, a as IQAuthRequestLike, b as IQAuthResponseLike, c as IQAuthNextFunction } from './types-XOV9XPVi.js';
2
- import { I as IQAuthClient } from './client-CDQ21LvW.js';
1
+ import { J as JwtClaims, f as IQAuthTokenClientConfig, X as ExpressMiddlewareOptions, a as IQAuthRequestLike, b as IQAuthResponseLike, c as IQAuthNextFunction } from './types-Bn8O-OEd.js';
2
+ import { I as IQAuthClient } from './client-DkPL0EPZ.js';
3
3
  export { E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.js';
4
- export { C as CookieAwareMiddlewareOptions, D as DEFAULT_ACCESS_COOKIE, a as DEFAULT_REFRESH_COOKIE, i as iqAuthMiddleware } from './express-Piv2WhWM.js';
4
+ export { C as CookieAwareMiddlewareOptions, D as DEFAULT_ACCESS_COOKIE, a as DEFAULT_REFRESH_COOKIE, i as iqAuthMiddleware } from './express-Budysq4h.js';
5
5
  export { HandlerResponse, IQAuthHelperConfig, SetCookieDirective, UserinfoResponse, buildUserinfoResponse, handleCallback, handleRefresh, handleSignout, handleUserinfo, serializeCookie } from './server/handlers.js';
6
- export { P as ProvisioningBridge, a as ProvisioningBridgeOptions, d as ProvisioningContext, b as ProvisioningStorage, c as createProvisioningBridge } from './provisioningBridge-CGpMRie4.js';
7
- import './tokens-Bqhmqq_R.js';
6
+ export { P as ProvisioningBridge, a as ProvisioningBridgeOptions, d as ProvisioningContext, e as ProvisioningError, b as ProvisioningStorage, c as createProvisioningBridge } from './provisioningBridge-BXPMZCLe.js';
7
+ import './tokens-9F6ETrzk.js';
8
8
 
9
9
  /**
10
10
  * linkLocalUserToIqAuthSub — first-time-migration helper.
@@ -31,9 +31,17 @@ import './tokens-Bqhmqq_R.js';
31
31
  * - 'linked' — wrote claims.sub onto the matched local row.
32
32
  * - 'already_linked' — a row was already keyed by claims.sub. No write.
33
33
  * - 'conflict' — matched row already has a different non-null sub,
34
- * OR more than one local row matches the email.
34
+ * OR more than one local row matches the email,
35
+ * OR the email is unverified and adoption is gated
36
+ * (reason 'unverified_email' — see H-4 below).
35
37
  * - 'not_found' — no local row matched any of the provided lookupBy
36
38
  * keys. Caller should provision a new row separately.
39
+ *
40
+ * Security (H-4): writing the IQAuth `sub` onto a pre-existing local row matched
41
+ * by email is a takeover of that account. It is only performed when the claims
42
+ * assert a verified email (`claims.email_verified === true`). When the email is
43
+ * unverified and `allowUnverifiedEmail` is not set, the helper fails closed with
44
+ * `{ status: 'conflict', reason: 'unverified_email' }` instead of linking.
37
45
  */
38
46
 
39
47
  type LinkLookupBy = "email";
@@ -46,7 +54,7 @@ type LinkResult = {
46
54
  } | {
47
55
  status: "conflict";
48
56
  userId?: string;
49
- reason: "different_sub" | "duplicate_email";
57
+ reason: "different_sub" | "duplicate_email" | "unverified_email";
50
58
  } | {
51
59
  status: "not_found";
52
60
  };
@@ -85,7 +93,7 @@ interface LinkAdapter {
85
93
  }
86
94
  interface LinkLocalUserOptions {
87
95
  adapter: LinkAdapter;
88
- claims: Pick<JwtClaims, "sub" | "email">;
96
+ claims: Pick<JwtClaims, "sub" | "email" | "email_verified">;
89
97
  /**
90
98
  * Lookup keys to try, in order. Currently only `'email'` is supported.
91
99
  * Defaults to `['email']`.
@@ -96,6 +104,18 @@ interface LinkLocalUserOptions {
96
104
  * but preserve the cased original at registration time.
97
105
  */
98
106
  caseInsensitiveEmail?: boolean;
107
+ /**
108
+ * Security gate (H-4): by default the email→sub link only proceeds when the
109
+ * claims assert a verified email (`claims.email_verified === true`). When the
110
+ * email is unverified and this flag is left `false`, the helper fails closed
111
+ * with `{ status: 'conflict', reason: 'unverified_email' }` rather than
112
+ * letting an unverified email take over a pre-existing local account.
113
+ *
114
+ * Set `true` ONLY when your issuer is trusted to never emit an unverified
115
+ * email for linking (or you have a compensating control). Defaults to
116
+ * `false` (secure).
117
+ */
118
+ allowUnverifiedEmail?: boolean;
99
119
  }
100
120
  declare function linkLocalUserToIqAuthSub(options: LinkLocalUserOptions): Promise<LinkResult>;
101
121
  interface DrizzleLikeDb {