@iqauth/sdk 2.6.4 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +173 -1
  2. package/dist/browser-session.d.mts +4 -4
  3. package/dist/browser-session.d.ts +4 -4
  4. package/dist/browser-session.js +212 -46
  5. package/dist/browser-session.mjs +3 -3
  6. package/dist/browser.d.mts +5 -5
  7. package/dist/browser.d.ts +5 -5
  8. package/dist/browser.js +293 -34
  9. package/dist/browser.mjs +5 -5
  10. package/dist/{chunk-BVV54LPI.mjs → chunk-25SSYDIP.mjs} +10 -4
  11. package/dist/{chunk-XAWYUPMO.mjs → chunk-4V7FKOTG.mjs} +242 -22
  12. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  13. package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
  14. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  15. package/dist/chunk-GLXSIGVS.mjs +66 -0
  16. package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
  17. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  18. package/dist/chunk-JRDVUWAL.mjs +46 -0
  19. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  20. package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
  21. package/dist/chunk-VYQ3ETCK.mjs +244 -0
  22. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  23. package/dist/chunk-WHT6WKTY.mjs +3180 -0
  24. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  25. package/dist/chunk-WSH4SW7F.mjs +490 -0
  26. package/dist/{chunk-W3F4JYGP.mjs → chunk-ZLJPABB7.mjs} +139 -23
  27. package/dist/cli/index.js +2 -2
  28. package/dist/cli/index.mjs +2 -2
  29. package/dist/{client-BNQe3AgF.d.ts → client-D8L-PaWr.d.mts} +59 -6
  30. package/dist/{client-kYlJFgPv.d.mts → client-DkPL0EPZ.d.ts} +59 -6
  31. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  32. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  33. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  34. package/dist/{express-CHpfa7D_.d.ts → express-Budysq4h.d.ts} +2 -2
  35. package/dist/{express-B6_1vBYZ.d.mts → express-DDTA3qV1.d.mts} +2 -2
  36. package/dist/express.d.mts +7 -6
  37. package/dist/express.d.ts +7 -6
  38. package/dist/express.js +563 -85
  39. package/dist/express.mjs +73 -34
  40. package/dist/fastify.d.mts +10 -0
  41. package/dist/fastify.d.ts +10 -0
  42. package/dist/fastify.js +589 -65
  43. package/dist/fastify.mjs +101 -11
  44. package/dist/hono.d.mts +10 -0
  45. package/dist/hono.d.ts +10 -0
  46. package/dist/hono.js +566 -65
  47. package/dist/hono.mjs +78 -11
  48. package/dist/index-Cko-d5po.d.mts +1848 -0
  49. package/dist/index-RNqwEcmY.d.ts +1848 -0
  50. package/dist/index.d.mts +56 -8
  51. package/dist/index.d.ts +56 -8
  52. package/dist/index.js +694 -75
  53. package/dist/index.mjs +30 -10
  54. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  55. package/dist/locales.d.mts +1 -1
  56. package/dist/locales.d.ts +1 -1
  57. package/dist/locales.js +36 -0
  58. package/dist/locales.mjs +1 -1
  59. package/dist/mobile.d.mts +77 -7
  60. package/dist/mobile.d.ts +77 -7
  61. package/dist/mobile.js +307 -46
  62. package/dist/mobile.mjs +98 -3
  63. package/dist/next.d.mts +10 -1
  64. package/dist/next.d.ts +10 -1
  65. package/dist/next.js +596 -205
  66. package/dist/next.mjs +83 -10
  67. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
  68. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
  69. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  70. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  71. package/dist/react-permissions.d.mts +52 -0
  72. package/dist/react-permissions.d.ts +52 -0
  73. package/dist/react-permissions.js +239 -0
  74. package/dist/react-permissions.mjs +98 -0
  75. package/dist/react.d.mts +9 -1624
  76. package/dist/react.d.ts +9 -1624
  77. package/dist/react.js +882 -73
  78. package/dist/react.mjs +71 -2631
  79. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  80. package/dist/server/handlers.d.mts +200 -4
  81. package/dist/server/handlers.d.ts +200 -4
  82. package/dist/server/handlers.js +530 -16
  83. package/dist/server/handlers.mjs +14 -3
  84. package/dist/server.d.mts +171 -8
  85. package/dist/server.d.ts +171 -8
  86. package/dist/server.js +579 -61
  87. package/dist/server.mjs +99 -12
  88. package/dist/service.d.mts +4 -4
  89. package/dist/service.d.ts +4 -4
  90. package/dist/service.js +212 -46
  91. package/dist/service.mjs +3 -3
  92. package/dist/{signIn-CiIBTJIh.d.mts → signIn-CReqfXsh.d.mts} +95 -3
  93. package/dist/{signIn-OCr88Zf8.d.ts → signIn-Cfa1GTpO.d.ts} +95 -3
  94. package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
  95. package/dist/test.mjs +3 -3
  96. package/dist/{tokens-DCyzzn8L.d.mts → tokens-9F6ETrzk.d.ts} +9 -2
  97. package/dist/{tokens-aHiGFr_E.d.ts → tokens-B06VtvUi.d.mts} +9 -2
  98. package/dist/{types-DZAflmmq.d.mts → types-Bn8O-OEd.d.mts} +164 -11
  99. package/dist/{types-DZAflmmq.d.ts → types-Bn8O-OEd.d.ts} +164 -11
  100. package/dist/{types-6bNdxesb.d.ts → types-DnU2LhXR.d.mts} +7 -1
  101. package/dist/{types-6bNdxesb.d.mts → types-DnU2LhXR.d.ts} +7 -1
  102. package/dist/webhooks.d.mts +113 -17
  103. package/dist/webhooks.d.ts +113 -17
  104. package/dist/webhooks.js +179 -15
  105. package/dist/webhooks.mjs +7 -1
  106. package/dist/ws.d.mts +2 -2
  107. package/dist/ws.d.ts +2 -2
  108. package/dist/ws.js +80 -30
  109. package/dist/ws.mjs +4 -4
  110. package/docs/error-handling.md +101 -0
  111. package/docs/guides/effective-permissions.md +171 -0
  112. package/docs/guides/invitations.md +65 -0
  113. package/package.json +19 -4
  114. package/dist/chunk-6TDJJER7.mjs +0 -217
  115. package/dist/chunk-UKZLOHZG.mjs +0 -83
  116. package/dist/errors-CDdl24MP.d.mts +0 -52
  117. package/dist/errors-CDdl24MP.d.ts +0 -52
@@ -4,8 +4,8 @@ import {
4
4
  exitImpersonation,
5
5
  reverify,
6
6
  withReverification
7
- } from "./chunk-LIZYFXH7.mjs";
8
- import "./chunk-6I6RM4MN.mjs";
7
+ } from "./chunk-DFWHSDYQ.mjs";
8
+ import "./chunk-6PJRLRB4.mjs";
9
9
  import "./chunk-Y6FXYEAI.mjs";
10
10
  export {
11
11
  PRIOR_SESSION_STORAGE_KEY,
@@ -1,3 +1,6 @@
1
+ import { c as TokenVerifyOptions } from '../tokens-B06VtvUi.mjs';
2
+ import { J as JwtClaims, S as SessionUser } from '../types-Bn8O-OEd.mjs';
3
+
1
4
  /**
2
5
  * Framework-neutral helper handlers for the auto-mounted routes added by the
3
6
  * @iqauth/sdk framework adapters. Each handler takes the parsed request
@@ -17,6 +20,35 @@
17
20
  * framework's cookie API. This keeps adapters trivially thin and makes the
18
21
  * handlers pure-functionally testable.
19
22
  */
23
+
24
+ /**
25
+ * Shape returned by the auto-mounted `GET /api/iqauth/me` route.
26
+ *
27
+ * Mirrors the envelope the browser SDK's `SessionManager.bootstrap()` already
28
+ * accepts: `data.user` is preferred when present (so an integrator can supply
29
+ * extra app-specific fields via `userinfoEnricher`), otherwise the SDK falls
30
+ * back to deriving a `SessionUser` from `data.claims` directly.
31
+ */
32
+ interface UserinfoResponse {
33
+ success: true;
34
+ data: {
35
+ user: SessionUser;
36
+ claims: JwtClaims;
37
+ tenantId: string | null;
38
+ };
39
+ }
40
+ /**
41
+ * Build the documented `/api/iqauth/me` envelope from a verified set of JWT
42
+ * claims. Pure / framework-neutral — exported so integrators replacing the
43
+ * route entirely can still emit the canonical shape.
44
+ *
45
+ * @param claims Verified JWT claims (output of `tokens.verify(token)`).
46
+ * @param opts.enrich Optional sync/async hook returning a partial
47
+ * `SessionUser` to merge over the claim-derived defaults.
48
+ */
49
+ declare function buildUserinfoResponse(claims: JwtClaims, opts?: {
50
+ enrich?: (claims: JwtClaims) => Partial<SessionUser> | Promise<Partial<SessionUser>>;
51
+ }): Promise<UserinfoResponse>;
20
52
  interface SetCookieDirective {
21
53
  name: string;
22
54
  value: string;
@@ -27,10 +59,21 @@ interface SetCookieDirective {
27
59
  sameSite: "lax" | "strict" | "none";
28
60
  path: string;
29
61
  domain?: string;
62
+ /**
63
+ * Marker for "this directive is clearing the cookie." When true, the
64
+ * serializer emits `Expires=Thu, 01 Jan 1970 00:00:00 GMT` in addition to
65
+ * `Max-Age=0` and an empty value. Some browsers (older Safari, certain
66
+ * embedded WebViews) ignore `Max-Age=0` without an explicit `Expires` in
67
+ * the past, which is the root cause of "ghost signed-in" sessions after
68
+ * Sign Out. Adapters with a typed cookie API (Express `res.clearCookie`,
69
+ * Fastify `reply.clearCookie`) should use the framework helper for the
70
+ * same belt-and-suspenders behavior.
71
+ */
72
+ clear?: boolean;
30
73
  }
31
74
  interface HandlerResponse {
32
75
  status: number;
33
- body: Record<string, unknown>;
76
+ body: Record<string, unknown> | UserinfoResponse;
34
77
  cookies: SetCookieDirective[];
35
78
  }
36
79
  interface IQAuthHelperConfig {
@@ -56,8 +99,21 @@ interface IQAuthHelperConfig {
56
99
  cookieDomain?: string;
57
100
  /** Cookie sameSite policy. Defaults to `lax`. */
58
101
  sameSite?: "lax" | "strict" | "none";
59
- /** 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
+ */
60
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;
61
117
  /** Cookie path. Defaults to `/`. */
62
118
  cookiePath?: string;
63
119
  /** Path of the OIDC token endpoint. */
@@ -86,8 +142,104 @@ interface IQAuthHelperConfig {
86
142
  * apps that manage cookie lifecycle entirely outside the SDK helpers.
87
143
  */
88
144
  clearCookiesOnRefreshFailure?: "terminal-only" | "always" | "never";
145
+ /**
146
+ * Mount the auto-mounted `GET ${mountPath}/me` userinfo route. Off by default
147
+ * in adapters until the integrator opts in. When true, the handler accepts
148
+ * either an `Authorization: Bearer …` header OR the `iqauth_at` cookie,
149
+ * verifies the token against the issuer, and returns the documented
150
+ * {@link UserinfoResponse} envelope (`data.user` + `data.claims` +
151
+ * `data.tenantId`).
152
+ *
153
+ * Independent of `mountHelperRoutes` so apps can opt into `/me` without
154
+ * also auto-mounting `/callback`/`/refresh`/`/signout` (e.g. if they
155
+ * already manage those routes themselves).
156
+ */
157
+ mountUserinfo?: boolean;
158
+ /**
159
+ * Optional hook to add app-specific fields to the `data.user` returned by
160
+ * the auto-mounted userinfo route. Receives the verified claims and the
161
+ * raw framework request object (typed `unknown` because adapters differ).
162
+ * Returned partial is shallow-merged over the claim-derived defaults so
163
+ * integrators can override e.g. `name`, or add fields not present on
164
+ * `SessionUser` itself (the response shape stays stable).
165
+ */
166
+ userinfoEnricher?: (claims: JwtClaims, req: unknown) => Partial<SessionUser> | Promise<Partial<SessionUser>>;
167
+ /**
168
+ * Token verification overrides forwarded to the cached `TokensModule` used
169
+ * by the userinfo handler. Mirrors the per-call shape on `tokens.verify`.
170
+ */
171
+ verify?: TokenVerifyOptions;
172
+ /**
173
+ * Task #126: Optional debug + timing-event hook. When `debug` is true, the
174
+ * helper handlers emit `console.debug("[iqauth_helper]", evt)` for each
175
+ * phase (`callback`, `refresh`, `signout`). When `onTimingEvent` is set,
176
+ * the same event is also passed to the callback, with `{ phase, durationMs,
177
+ * ok, code? }` shape. Use to push timings into your APM (Datadog, OTEL).
178
+ */
179
+ debug?: boolean;
180
+ onTimingEvent?: (event: IQAuthTimingEvent) => void;
181
+ /**
182
+ * Pluggable registry that tracks recently-signed-out idempotency tokens
183
+ * (synthetic per-session opaque strings; see {@link SignoutRegistry}).
184
+ * Defaults to a module-scoped in-memory `Map` shared by every adapter
185
+ * instance in this process. Multi-instance deployments (Railway replicas,
186
+ * autoscaled containers, blue/green) MUST plug in a shared store
187
+ * (Redis is the obvious fit) so a refresh routed to instance B can see
188
+ * the signout that landed on instance A. The interface is intentionally
189
+ * tiny — `mark` records the token with a TTL, `has` queries it.
190
+ */
191
+ signoutRegistry?: SignoutRegistry;
192
+ /**
193
+ * TTL applied when {@link handleSignout} marks an idempotency token.
194
+ * Defaults to 60 seconds — long enough to swallow any reasonable
195
+ * refresh-then-signout race, short enough that it can never delay a
196
+ * legitimate fresh sign-in.
197
+ */
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;
223
+ }
224
+ interface IQAuthTimingEvent {
225
+ phase: "callback" | "refresh" | "signout" | "bootstrap" | "signIn";
226
+ durationMs: number;
227
+ ok: boolean;
228
+ code?: string;
229
+ }
230
+ /**
231
+ * Pluggable backing store for the refresh/signout collapse cache. The
232
+ * default in-memory implementation is process-local and works fine for
233
+ * single-process deployments; multi-instance deployments should provide a
234
+ * shared backend (Redis is the canonical choice — `mark` ⇒ `SETEX`,
235
+ * `has` ⇒ `EXISTS`). Both methods may be sync or async; handlers always
236
+ * `await` the result.
237
+ */
238
+ interface SignoutRegistry {
239
+ mark(idempotencyToken: string, ttlMs: number): void | Promise<void>;
240
+ has(idempotencyToken: string): boolean | Promise<boolean>;
89
241
  }
90
- interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure" | "cookieNames">> {
242
+ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure" | "cookieNames" | "mountUserinfo" | "userinfoEnricher" | "verify" | "debug" | "onTimingEvent" | "signoutRegistry" | "signoutMarkerTtlMs" | "allowInsecureCookies">> {
91
243
  cookieNames?: {
92
244
  access?: string;
93
245
  refresh?: string;
@@ -99,7 +251,29 @@ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" |
99
251
  appId: string;
100
252
  tenantId: string;
101
253
  clearCookiesOnRefreshFailure: "terminal-only" | "always" | "never";
254
+ debug?: boolean;
255
+ onTimingEvent?: (event: IQAuthTimingEvent) => void;
256
+ signoutRegistry: SignoutRegistry;
257
+ signoutMarkerTtlMs: number;
102
258
  }
259
+ /**
260
+ * Test-only hook for clearing the default in-memory signout marker cache
261
+ * between test runs. Custom registries are unaffected. Not part of the
262
+ * public surface.
263
+ */
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;
271
+ /**
272
+ * Public factory for a fresh in-memory {@link SignoutRegistry}. Useful for
273
+ * tests that want isolated state per case, and as a reference implementation
274
+ * for apps building Redis-backed (or other shared-store) registries.
275
+ */
276
+ declare function createInMemorySignoutRegistry(): SignoutRegistry;
103
277
  /**
104
278
  * Serialize a cookie directive to a Set-Cookie header value. Adapters that
105
279
  * lack a typed cookie API (Hono, raw Node) use this; Express / Fastify /
@@ -111,6 +285,14 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
111
285
  code?: string;
112
286
  codeVerifier?: string;
113
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;
114
296
  }): Promise<HandlerResponse>;
115
297
  /** POST /api/iqauth/refresh — rotate refresh + access cookies.
116
298
  *
@@ -123,12 +305,26 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
123
305
  */
124
306
  declare function handleRefresh(config: IQAuthHelperConfig, input: {
125
307
  refreshToken?: string;
308
+ idempotencyToken?: string;
126
309
  }): Promise<HandlerResponse>;
127
310
  /** POST /api/iqauth/signout — clear cookies and best-effort revoke at issuer. */
128
311
  declare function handleSignout(config: IQAuthHelperConfig, input: {
129
312
  accessToken?: string;
313
+ refreshToken?: string;
314
+ idempotencyToken?: string;
130
315
  ssoCookieHeader?: string;
131
316
  endSsoSession?: boolean;
132
317
  }): Promise<HandlerResponse>;
318
+ /**
319
+ * GET /api/iqauth/me — verify the access token and return the documented
320
+ * userinfo envelope. Accepts the token from `Authorization: Bearer …` OR
321
+ * the access cookie (default `iqauth_at`). Adapters call this directly; the
322
+ * `req` value is forwarded verbatim to `userinfoEnricher` so integrators can
323
+ * read additional context (headers, sub-paths, etc).
324
+ */
325
+ declare function handleUserinfo(config: IQAuthHelperConfig, input: {
326
+ accessToken?: string;
327
+ req?: unknown;
328
+ }): Promise<HandlerResponse>;
133
329
 
134
- export { type HandlerResponse, type IQAuthHelperConfig, type ResolvedConfig as ResolvedIQAuthHelperConfig, type SetCookieDirective, handleCallback, handleRefresh, handleSignout, 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,3 +1,6 @@
1
+ import { c as TokenVerifyOptions } from '../tokens-9F6ETrzk.js';
2
+ import { J as JwtClaims, S as SessionUser } from '../types-Bn8O-OEd.js';
3
+
1
4
  /**
2
5
  * Framework-neutral helper handlers for the auto-mounted routes added by the
3
6
  * @iqauth/sdk framework adapters. Each handler takes the parsed request
@@ -17,6 +20,35 @@
17
20
  * framework's cookie API. This keeps adapters trivially thin and makes the
18
21
  * handlers pure-functionally testable.
19
22
  */
23
+
24
+ /**
25
+ * Shape returned by the auto-mounted `GET /api/iqauth/me` route.
26
+ *
27
+ * Mirrors the envelope the browser SDK's `SessionManager.bootstrap()` already
28
+ * accepts: `data.user` is preferred when present (so an integrator can supply
29
+ * extra app-specific fields via `userinfoEnricher`), otherwise the SDK falls
30
+ * back to deriving a `SessionUser` from `data.claims` directly.
31
+ */
32
+ interface UserinfoResponse {
33
+ success: true;
34
+ data: {
35
+ user: SessionUser;
36
+ claims: JwtClaims;
37
+ tenantId: string | null;
38
+ };
39
+ }
40
+ /**
41
+ * Build the documented `/api/iqauth/me` envelope from a verified set of JWT
42
+ * claims. Pure / framework-neutral — exported so integrators replacing the
43
+ * route entirely can still emit the canonical shape.
44
+ *
45
+ * @param claims Verified JWT claims (output of `tokens.verify(token)`).
46
+ * @param opts.enrich Optional sync/async hook returning a partial
47
+ * `SessionUser` to merge over the claim-derived defaults.
48
+ */
49
+ declare function buildUserinfoResponse(claims: JwtClaims, opts?: {
50
+ enrich?: (claims: JwtClaims) => Partial<SessionUser> | Promise<Partial<SessionUser>>;
51
+ }): Promise<UserinfoResponse>;
20
52
  interface SetCookieDirective {
21
53
  name: string;
22
54
  value: string;
@@ -27,10 +59,21 @@ interface SetCookieDirective {
27
59
  sameSite: "lax" | "strict" | "none";
28
60
  path: string;
29
61
  domain?: string;
62
+ /**
63
+ * Marker for "this directive is clearing the cookie." When true, the
64
+ * serializer emits `Expires=Thu, 01 Jan 1970 00:00:00 GMT` in addition to
65
+ * `Max-Age=0` and an empty value. Some browsers (older Safari, certain
66
+ * embedded WebViews) ignore `Max-Age=0` without an explicit `Expires` in
67
+ * the past, which is the root cause of "ghost signed-in" sessions after
68
+ * Sign Out. Adapters with a typed cookie API (Express `res.clearCookie`,
69
+ * Fastify `reply.clearCookie`) should use the framework helper for the
70
+ * same belt-and-suspenders behavior.
71
+ */
72
+ clear?: boolean;
30
73
  }
31
74
  interface HandlerResponse {
32
75
  status: number;
33
- body: Record<string, unknown>;
76
+ body: Record<string, unknown> | UserinfoResponse;
34
77
  cookies: SetCookieDirective[];
35
78
  }
36
79
  interface IQAuthHelperConfig {
@@ -56,8 +99,21 @@ interface IQAuthHelperConfig {
56
99
  cookieDomain?: string;
57
100
  /** Cookie sameSite policy. Defaults to `lax`. */
58
101
  sameSite?: "lax" | "strict" | "none";
59
- /** 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
+ */
60
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;
61
117
  /** Cookie path. Defaults to `/`. */
62
118
  cookiePath?: string;
63
119
  /** Path of the OIDC token endpoint. */
@@ -86,8 +142,104 @@ interface IQAuthHelperConfig {
86
142
  * apps that manage cookie lifecycle entirely outside the SDK helpers.
87
143
  */
88
144
  clearCookiesOnRefreshFailure?: "terminal-only" | "always" | "never";
145
+ /**
146
+ * Mount the auto-mounted `GET ${mountPath}/me` userinfo route. Off by default
147
+ * in adapters until the integrator opts in. When true, the handler accepts
148
+ * either an `Authorization: Bearer …` header OR the `iqauth_at` cookie,
149
+ * verifies the token against the issuer, and returns the documented
150
+ * {@link UserinfoResponse} envelope (`data.user` + `data.claims` +
151
+ * `data.tenantId`).
152
+ *
153
+ * Independent of `mountHelperRoutes` so apps can opt into `/me` without
154
+ * also auto-mounting `/callback`/`/refresh`/`/signout` (e.g. if they
155
+ * already manage those routes themselves).
156
+ */
157
+ mountUserinfo?: boolean;
158
+ /**
159
+ * Optional hook to add app-specific fields to the `data.user` returned by
160
+ * the auto-mounted userinfo route. Receives the verified claims and the
161
+ * raw framework request object (typed `unknown` because adapters differ).
162
+ * Returned partial is shallow-merged over the claim-derived defaults so
163
+ * integrators can override e.g. `name`, or add fields not present on
164
+ * `SessionUser` itself (the response shape stays stable).
165
+ */
166
+ userinfoEnricher?: (claims: JwtClaims, req: unknown) => Partial<SessionUser> | Promise<Partial<SessionUser>>;
167
+ /**
168
+ * Token verification overrides forwarded to the cached `TokensModule` used
169
+ * by the userinfo handler. Mirrors the per-call shape on `tokens.verify`.
170
+ */
171
+ verify?: TokenVerifyOptions;
172
+ /**
173
+ * Task #126: Optional debug + timing-event hook. When `debug` is true, the
174
+ * helper handlers emit `console.debug("[iqauth_helper]", evt)` for each
175
+ * phase (`callback`, `refresh`, `signout`). When `onTimingEvent` is set,
176
+ * the same event is also passed to the callback, with `{ phase, durationMs,
177
+ * ok, code? }` shape. Use to push timings into your APM (Datadog, OTEL).
178
+ */
179
+ debug?: boolean;
180
+ onTimingEvent?: (event: IQAuthTimingEvent) => void;
181
+ /**
182
+ * Pluggable registry that tracks recently-signed-out idempotency tokens
183
+ * (synthetic per-session opaque strings; see {@link SignoutRegistry}).
184
+ * Defaults to a module-scoped in-memory `Map` shared by every adapter
185
+ * instance in this process. Multi-instance deployments (Railway replicas,
186
+ * autoscaled containers, blue/green) MUST plug in a shared store
187
+ * (Redis is the obvious fit) so a refresh routed to instance B can see
188
+ * the signout that landed on instance A. The interface is intentionally
189
+ * tiny — `mark` records the token with a TTL, `has` queries it.
190
+ */
191
+ signoutRegistry?: SignoutRegistry;
192
+ /**
193
+ * TTL applied when {@link handleSignout} marks an idempotency token.
194
+ * Defaults to 60 seconds — long enough to swallow any reasonable
195
+ * refresh-then-signout race, short enough that it can never delay a
196
+ * legitimate fresh sign-in.
197
+ */
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;
223
+ }
224
+ interface IQAuthTimingEvent {
225
+ phase: "callback" | "refresh" | "signout" | "bootstrap" | "signIn";
226
+ durationMs: number;
227
+ ok: boolean;
228
+ code?: string;
229
+ }
230
+ /**
231
+ * Pluggable backing store for the refresh/signout collapse cache. The
232
+ * default in-memory implementation is process-local and works fine for
233
+ * single-process deployments; multi-instance deployments should provide a
234
+ * shared backend (Redis is the canonical choice — `mark` ⇒ `SETEX`,
235
+ * `has` ⇒ `EXISTS`). Both methods may be sync or async; handlers always
236
+ * `await` the result.
237
+ */
238
+ interface SignoutRegistry {
239
+ mark(idempotencyToken: string, ttlMs: number): void | Promise<void>;
240
+ has(idempotencyToken: string): boolean | Promise<boolean>;
89
241
  }
90
- interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure" | "cookieNames">> {
242
+ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" | "cookieDomain" | "issuer" | "fetchImpl" | "clearCookiesOnRefreshFailure" | "cookieNames" | "mountUserinfo" | "userinfoEnricher" | "verify" | "debug" | "onTimingEvent" | "signoutRegistry" | "signoutMarkerTtlMs" | "allowInsecureCookies">> {
91
243
  cookieNames?: {
92
244
  access?: string;
93
245
  refresh?: string;
@@ -99,7 +251,29 @@ interface ResolvedConfig extends Required<Omit<IQAuthHelperConfig, "secretKey" |
99
251
  appId: string;
100
252
  tenantId: string;
101
253
  clearCookiesOnRefreshFailure: "terminal-only" | "always" | "never";
254
+ debug?: boolean;
255
+ onTimingEvent?: (event: IQAuthTimingEvent) => void;
256
+ signoutRegistry: SignoutRegistry;
257
+ signoutMarkerTtlMs: number;
102
258
  }
259
+ /**
260
+ * Test-only hook for clearing the default in-memory signout marker cache
261
+ * between test runs. Custom registries are unaffected. Not part of the
262
+ * public surface.
263
+ */
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;
271
+ /**
272
+ * Public factory for a fresh in-memory {@link SignoutRegistry}. Useful for
273
+ * tests that want isolated state per case, and as a reference implementation
274
+ * for apps building Redis-backed (or other shared-store) registries.
275
+ */
276
+ declare function createInMemorySignoutRegistry(): SignoutRegistry;
103
277
  /**
104
278
  * Serialize a cookie directive to a Set-Cookie header value. Adapters that
105
279
  * lack a typed cookie API (Hono, raw Node) use this; Express / Fastify /
@@ -111,6 +285,14 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
111
285
  code?: string;
112
286
  codeVerifier?: string;
113
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;
114
296
  }): Promise<HandlerResponse>;
115
297
  /** POST /api/iqauth/refresh — rotate refresh + access cookies.
116
298
  *
@@ -123,12 +305,26 @@ declare function handleCallback(config: IQAuthHelperConfig, input: {
123
305
  */
124
306
  declare function handleRefresh(config: IQAuthHelperConfig, input: {
125
307
  refreshToken?: string;
308
+ idempotencyToken?: string;
126
309
  }): Promise<HandlerResponse>;
127
310
  /** POST /api/iqauth/signout — clear cookies and best-effort revoke at issuer. */
128
311
  declare function handleSignout(config: IQAuthHelperConfig, input: {
129
312
  accessToken?: string;
313
+ refreshToken?: string;
314
+ idempotencyToken?: string;
130
315
  ssoCookieHeader?: string;
131
316
  endSsoSession?: boolean;
132
317
  }): Promise<HandlerResponse>;
318
+ /**
319
+ * GET /api/iqauth/me — verify the access token and return the documented
320
+ * userinfo envelope. Accepts the token from `Authorization: Bearer …` OR
321
+ * the access cookie (default `iqauth_at`). Adapters call this directly; the
322
+ * `req` value is forwarded verbatim to `userinfoEnricher` so integrators can
323
+ * read additional context (headers, sub-paths, etc).
324
+ */
325
+ declare function handleUserinfo(config: IQAuthHelperConfig, input: {
326
+ accessToken?: string;
327
+ req?: unknown;
328
+ }): Promise<HandlerResponse>;
133
329
 
134
- export { type HandlerResponse, type IQAuthHelperConfig, type ResolvedConfig as ResolvedIQAuthHelperConfig, type SetCookieDirective, handleCallback, handleRefresh, handleSignout, 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 };