@iqauth/sdk 2.6.3 → 2.7.0

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 (112) 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 +181 -41
  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 +271 -32
  9. package/dist/browser.mjs +10 -8
  10. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  11. package/dist/chunk-C2ZTBOAC.mjs +36 -0
  12. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  13. package/dist/chunk-GLXSIGVS.mjs +66 -0
  14. package/dist/{chunk-TKZTCPEK.mjs → chunk-GN37E64I.mjs} +32 -40
  15. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  16. package/dist/{chunk-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
  17. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  18. package/dist/chunk-PMAFENVI.mjs +229 -0
  19. package/dist/chunk-RR2MGPTK.mjs +2724 -0
  20. package/dist/{chunk-76W5TLQQ.mjs → chunk-RTJAIBXY.mjs} +220 -20
  21. package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
  22. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  23. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  24. package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
  25. package/dist/cli/index.js +2 -2
  26. package/dist/cli/index.mjs +2 -2
  27. package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
  28. package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
  29. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  30. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  31. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  32. package/dist/{express-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
  33. package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
  34. package/dist/express.d.mts +7 -6
  35. package/dist/express.d.ts +7 -6
  36. package/dist/express.js +349 -52
  37. package/dist/express.mjs +39 -12
  38. package/dist/fastify.d.mts +2 -0
  39. package/dist/fastify.d.ts +2 -0
  40. package/dist/fastify.js +332 -52
  41. package/dist/fastify.mjs +23 -8
  42. package/dist/hono.d.mts +2 -0
  43. package/dist/hono.d.ts +2 -0
  44. package/dist/hono.js +329 -52
  45. package/dist/hono.mjs +20 -8
  46. package/dist/index-5KSZEnDe.d.ts +1626 -0
  47. package/dist/index-CKoZHAoc.d.mts +1626 -0
  48. package/dist/index.d.mts +56 -8
  49. package/dist/index.d.ts +56 -8
  50. package/dist/index.js +565 -69
  51. package/dist/index.mjs +29 -9
  52. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  53. package/dist/locales.d.mts +1 -1
  54. package/dist/locales.d.ts +1 -1
  55. package/dist/mobile.d.mts +77 -7
  56. package/dist/mobile.d.ts +77 -7
  57. package/dist/mobile.js +276 -41
  58. package/dist/mobile.mjs +98 -3
  59. package/dist/next.d.mts +2 -1
  60. package/dist/next.d.ts +2 -1
  61. package/dist/next.js +391 -201
  62. package/dist/next.mjs +22 -7
  63. package/dist/pkce-7WKV4OIN.mjs +11 -0
  64. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
  65. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
  66. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  67. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  68. package/dist/react-permissions.d.mts +52 -0
  69. package/dist/react-permissions.d.ts +52 -0
  70. package/dist/react-permissions.js +239 -0
  71. package/dist/react-permissions.mjs +97 -0
  72. package/dist/react.d.mts +9 -1624
  73. package/dist/react.d.ts +9 -1624
  74. package/dist/react.js +343 -36
  75. package/dist/react.mjs +59 -2611
  76. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  77. package/dist/server/handlers.d.mts +148 -3
  78. package/dist/server/handlers.d.ts +148 -3
  79. package/dist/server/handlers.js +410 -11
  80. package/dist/server/handlers.mjs +12 -3
  81. package/dist/server.d.mts +151 -8
  82. package/dist/server.d.ts +151 -8
  83. package/dist/server.js +406 -50
  84. package/dist/server.mjs +93 -11
  85. package/dist/service.d.mts +4 -4
  86. package/dist/service.d.ts +4 -4
  87. package/dist/service.js +181 -41
  88. package/dist/service.mjs +3 -3
  89. package/dist/{signIn-CiIBTJIh.d.mts → signIn-BLFnz8SV.d.ts} +78 -3
  90. package/dist/{signIn-CCY4JE5G.mjs → signIn-SHBW6Z4T.mjs} +2 -1
  91. package/dist/{signIn-OCr88Zf8.d.ts → signIn-T-CZ6t6r.d.mts} +78 -3
  92. package/dist/test.mjs +3 -3
  93. package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
  94. package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
  95. package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
  96. package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
  97. package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
  98. package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
  99. package/dist/webhooks.d.mts +100 -17
  100. package/dist/webhooks.d.ts +100 -17
  101. package/dist/webhooks.js +164 -15
  102. package/dist/webhooks.mjs +7 -1
  103. package/dist/ws.d.mts +2 -2
  104. package/dist/ws.d.ts +2 -2
  105. package/dist/ws.js +80 -30
  106. package/dist/ws.mjs +4 -4
  107. package/docs/error-handling.md +101 -0
  108. package/docs/guides/effective-permissions.md +171 -0
  109. package/package.json +13 -3
  110. package/dist/chunk-UKZLOHZG.mjs +0 -83
  111. package/dist/errors-CDdl24MP.d.mts +0 -52
  112. package/dist/errors-CDdl24MP.d.ts +0 -52
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  The canonical TypeScript SDK for **IQAuthService** — DispositionIQ's multi-tenant identity and authorization platform. One package covers the React client, the four major Node frameworks (Express, Fastify, Hono, Next.js), native mobile, and headless service automation.
10
10
 
11
- > **New in 2.0.3** — `SessionManager` gains `serverManagedSession: true` for confidential-client apps, and the cookie-managed `/refresh` helper no longer wipes cookies on transient failures. See [What's new in 2.0.3](#whats-new-in-203).
11
+ > **New in 2.7.0** — `IQAuthError` now exposes a typed `code` taxonomy (`token_expired`, `jwks_unavailable`, `rate_limited`, …) plus `err.is(code)` / `IQAuthError.isIQAuthError(value)` helpers, and `tokens.verify<T>()` accepts a custom-claims generic so the returned object is fully typed. Both changes are purely additive — existing `catch (e: Error)` and untyped `verify()` callers keep compiling. See [What's new in 2.7.0](#whats-new-in-270).
12
12
 
13
13
  ---
14
14
 
@@ -17,6 +17,8 @@ The canonical TypeScript SDK for **IQAuthService** — DispositionIQ's multi-ten
17
17
  - [Install](#install)
18
18
  - [Five-line integration](#five-line-integration)
19
19
  - [Pick your environment](#pick-your-environment)
20
+ - [What's new in 2.7.0](#whats-new-in-270)
21
+ - [What's new in 2.6.5](#whats-new-in-265)
20
22
  - [What's new in 2.6.2](#whats-new-in-262)
21
23
  - [What's new in 2.6.1](#whats-new-in-261)
22
24
  - [What's new in 2.0.3](#whats-new-in-203)
@@ -174,6 +176,121 @@ The SDK is not "one auth model for every environment". The safe pattern depends
174
176
 
175
177
  ---
176
178
 
179
+ ## What's new in 2.7.0
180
+
181
+ ### 1. Typed `IQAuthError` taxonomy
182
+
183
+ Every SDK-originated throw now carries a `code` from a fixed 10-value union,
184
+ so callers can stop string-matching on `err.message` or guessing whether
185
+ `err.code` is upper-snake or lowercase. Two helpers ship alongside it:
186
+ `IQAuthError.isIQAuthError(value)` (instanceof-safe across realms) and
187
+ `err.is(code)` (narrow-friendly).
188
+
189
+ ```ts
190
+ import { IQAuthError, type IQAuthErrorCode } from "@iqauth/sdk";
191
+
192
+ try {
193
+ const claims = await client.tokens.verify(token);
194
+ } catch (err) {
195
+ if (IQAuthError.isIQAuthError(err)) {
196
+ if (err.is("token_expired")) return refreshAndRetry();
197
+ if (err.is("jwks_fetch_failed")) return retryAfterBackoff();
198
+ if (err.is("rate_limited")) return showRateLimitToast();
199
+ if (err.is("network")) return showOfflineBanner();
200
+ if (err.is("config_invalid")) throw err; // boot-time misconfig
201
+ }
202
+ throw err;
203
+ }
204
+ ```
205
+
206
+ The full union:
207
+
208
+ ```ts
209
+ type IQAuthErrorCode =
210
+ | "token_expired"
211
+ | "token_invalid"
212
+ | "jwks_unavailable"
213
+ | "jwks_fetch_failed"
214
+ | "rate_limited"
215
+ | "network"
216
+ | "config_invalid"
217
+ | "app_not_found"
218
+ | "permission_denied"
219
+ | "unknown";
220
+ ```
221
+
222
+ **Back-compat:** the field is widened to `IQAuthErrorCode | (string & {})`,
223
+ so server-rethrown codes (`TOKEN_REVOKED`, `SESSION_EXPIRED_INACTIVITY`, …)
224
+ still flow through unchanged. The framework adapters (`/express`,
225
+ `/fastify`, `/hono`) map both upper-snake and the new lowercase codes to
226
+ `401`, so this rollout is invisible to existing app code. `IQAuthError`
227
+ also gains a `cause` accessor (alias for the legacy `raw`).
228
+
229
+ See [`docs/error-handling.md`](./docs/error-handling.md) for the full
230
+ recipe book.
231
+
232
+ ### 2. `IQAuthClaims<T>` generic on `tokens.verify`
233
+
234
+ `verify()` now accepts a custom-claims generic so your app's bespoke
235
+ claims show up *typed* on the result — no index-signature widening, no
236
+ `as any`:
237
+
238
+ ```ts
239
+ interface MyClaims { plan: "free" | "pro"; orgId: string }
240
+
241
+ const claims = await client.tokens.verify<MyClaims>(token);
242
+ // ^? IQAuthBaseClaims & MyClaims & JwtClaims
243
+
244
+ if (claims.plan === "pro") doProThing(claims.orgId);
245
+ console.log(claims.tenantId, claims.sub); // base claims still typed
246
+ ```
247
+
248
+ `IQAuthBaseClaims` is exported separately for callers composing their own
249
+ envelope. `JwtClaims` continues to be exported and remains the return
250
+ type of `tokens.decode()` / `tokens.getClaims()` for back-compat. Calls
251
+ to `verify()` without a generic argument behave exactly as before.
252
+
253
+ ---
254
+
255
+ ## What's new in 2.6.5
256
+
257
+ ### Server-managed userinfo (`mountUserinfo: true`)
258
+
259
+ The framework adapters can now auto-mount `GET /api/iqauth/me` so
260
+ server-managed integrators don't have to hand-roll a userinfo handler
261
+ that calls `tokens.verify` and shapes a `data.user` / `data.claims`
262
+ envelope.
263
+
264
+ ```ts
265
+ app.use(iqAuth({
266
+ publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
267
+ secretKey: process.env.IQAUTH_SECRET_KEY!,
268
+ mountUserinfo: true,
269
+ // Optional — shallow-merged over the claim-derived SessionUser defaults.
270
+ userinfoEnricher: async (claims, req) => ({
271
+ name: await loadDisplayName(claims.sub),
272
+ }),
273
+ }));
274
+ ```
275
+
276
+ Returns the documented `UserinfoResponse` envelope —
277
+ `{ success: true, data: { user, claims, tenantId } }` — which is exactly
278
+ the shape the browser SDK's `SessionManager.bootstrap()` already accepts
279
+ (`data.user` is preferred; `claimsToSessionUser(data.claims)` is the
280
+ fallback). The token is read from `Authorization: Bearer …` OR the
281
+ `iqauth_at` cookie; verification reuses a per-issuer cached
282
+ `TokensModule` so JWKS fetches are amortized.
283
+
284
+ Two new framework-neutral exports for integrators who'd rather mount
285
+ their own route but still emit the canonical envelope:
286
+ `buildUserinfoResponse(claims, { enrich? })` and
287
+ `handleUserinfo(config, { accessToken, req? })`. Re-exported from both
288
+ `@iqauth/sdk` and `@iqauth/sdk/server`. See the
289
+ [Server-managed userinfo](#server-managed-userinfo) section below for
290
+ the full reference.
291
+
292
+ ---
293
+
177
294
  ## What's new in 2.6.2
178
295
 
179
296
  ### Card grows responsively on desktop (no more phone-sized form)
@@ -381,6 +498,7 @@ app.use("/admin",
381
498
  | `GET` | `/api/iqauth/callback` | Receives the OIDC redirect, exchanges the code, sets `iqauth_at` + `iqauth_rt`, redirects to `return_to` |
382
499
  | `POST` | `/api/iqauth/refresh` | Reads `iqauth_rt`, rotates, sets new cookies. Honors `clearCookiesOnRefreshFailure` |
383
500
  | `POST` | `/api/iqauth/signout` | Revokes the refresh token upstream and clears both cookies |
501
+ | `GET` | `/api/iqauth/me` | **Opt-in via `mountUserinfo: true`.** Verifies the access token and returns the documented userinfo envelope. See [Server-managed userinfo](#server-managed-userinfo) |
384
502
 
385
503
  All three set cookies with `HttpOnly; Secure; SameSite=lax; Path=/` by default. Override per-app:
386
504
 
@@ -398,6 +516,60 @@ iqAuth({
398
516
 
399
517
  ---
400
518
 
519
+ ### Server-managed userinfo
520
+
521
+ When you're doing the cookie-managed pattern (browser SDK proxies through
522
+ your own backend), your frontend needs *some* endpoint to learn "who am I"
523
+ on first paint. Before 2.6.5 you had to hand-roll that handler — call
524
+ `tokens.verify`, shape a `data.user` envelope, and remember to read the
525
+ token from either `Authorization: Bearer …` OR the `iqauth_at` cookie.
526
+
527
+ Opt into the auto-mounted route instead:
528
+
529
+ ```ts
530
+ app.use(iqAuth({
531
+ publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
532
+ secretKey: process.env.IQAUTH_SECRET_KEY!,
533
+ mountUserinfo: true,
534
+ // Optional — shallow-merged over the claim-derived SessionUser defaults.
535
+ userinfoEnricher: async (claims, req) => ({
536
+ name: await loadDisplayName(claims.sub),
537
+ }),
538
+ }));
539
+ ```
540
+
541
+ `GET /api/iqauth/me` returns the documented `UserinfoResponse` envelope:
542
+
543
+ ```jsonc
544
+ {
545
+ "success": true,
546
+ "data": {
547
+ "user": { "sub": "...", "email": "...", "name": "...", "tenantId": "...", "roles": [...], "entitlements": [...] },
548
+ "claims": { /* full verified JWT payload */ },
549
+ "tenantId": "ten_..." // or null
550
+ }
551
+ }
552
+ ```
553
+
554
+ This is exactly the shape `SessionManager.bootstrap()` already accepts:
555
+ `data.user` is preferred when present, `claimsToSessionUser(data.claims)`
556
+ is the documented fallback. Same option works on Express, Fastify, Hono,
557
+ and the Next.js handler. Token is verified with a per-issuer cached
558
+ `TokensModule` so JWKS fetches are amortized across requests.
559
+
560
+ Want to mount your own route but still emit the canonical envelope? Use
561
+ the framework-neutral helpers:
562
+
563
+ ```ts
564
+ import { buildUserinfoResponse, type UserinfoResponse } from "@iqauth/sdk/server";
565
+
566
+ const envelope: UserinfoResponse = await buildUserinfoResponse(verifiedClaims, {
567
+ enrich: (c) => ({ name: lookupName(c.sub) }),
568
+ });
569
+ ```
570
+
571
+ ---
572
+
401
573
  ## Token verification without a framework adapter
402
574
 
403
575
  If you're not using Express/Fastify/Hono/Next (custom Node server, AWS Lambda, Cloudflare Worker, etc.):
@@ -1,7 +1,7 @@
1
- import { c as IQAuthBrowserSessionClientConfig, d as SessionUser } from './types-DZAflmmq.mjs';
2
- import { I as IQAuthClient } from './client-kYlJFgPv.mjs';
3
- export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.mjs';
4
- import './tokens-DCyzzn8L.mjs';
1
+ import { I as IQAuthBrowserSessionClientConfig, S as SessionUser } from './types-XOV9XPVi.mjs';
2
+ import { I as IQAuthClient } from './client-BGFnBpfc.mjs';
3
+ export { E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.mjs';
4
+ import './tokens-CITeoG6P.mjs';
5
5
 
6
6
  declare class BrowserSessionIQAuthClient extends IQAuthClient {
7
7
  constructor(config: Omit<IQAuthBrowserSessionClientConfig, "environment">);
@@ -1,7 +1,7 @@
1
- import { c as IQAuthBrowserSessionClientConfig, d as SessionUser } from './types-DZAflmmq.js';
2
- import { I as IQAuthClient } from './client-BNQe3AgF.js';
3
- export { E as ErrorCodes, I as IQAuthError } from './errors-CDdl24MP.js';
4
- import './tokens-aHiGFr_E.js';
1
+ import { I as IQAuthBrowserSessionClientConfig, S as SessionUser } from './types-XOV9XPVi.js';
2
+ import { I as IQAuthClient } from './client-CDQ21LvW.js';
3
+ export { E as ErrorCodes, I as IQAuthError } from './errors-Jl1Jtm-6.js';
4
+ import './tokens-Bqhmqq_R.js';
5
5
 
6
6
  declare class BrowserSessionIQAuthClient extends IQAuthClient {
7
7
  constructor(config: Omit<IQAuthBrowserSessionClientConfig, "environment">);
@@ -39,13 +39,30 @@ __export(browser_session_exports, {
39
39
  module.exports = __toCommonJS(browser_session_exports);
40
40
 
41
41
  // src/errors.ts
42
- var IQAuthError = class extends Error {
43
- constructor(code, message, status, raw) {
42
+ var IQAuthError = class _IQAuthError extends Error {
43
+ constructor(code, message, status, cause) {
44
44
  super(message);
45
45
  this.name = "IQAuthError";
46
46
  this.code = code;
47
47
  this.status = status;
48
- this.raw = raw;
48
+ this.cause = cause;
49
+ this.raw = cause;
50
+ }
51
+ /**
52
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
53
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
54
+ */
55
+ static isIQAuthError(value) {
56
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
57
+ }
58
+ /**
59
+ * Type-narrowed code check. Lets callers write
60
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
61
+ * taxonomy without losing the ability to handle server codes via
62
+ * `err.code === "TOKEN_REVOKED"`.
63
+ */
64
+ is(code) {
65
+ return this.code === code;
49
66
  }
50
67
  };
51
68
  var ErrorCodes = {
@@ -196,7 +213,7 @@ var HttpClient = class {
196
213
  headers: this.buildHeaders(),
197
214
  ...this.isBrowserSession() ? { credentials: "include" } : (() => {
198
215
  const refreshToken = this.config.getRefreshToken();
199
- if (!refreshToken) throw new IQAuthError("TOKEN_INVALID", "No refresh token available");
216
+ if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
200
217
  return { body: JSON.stringify({ refreshToken }) };
201
218
  })()
202
219
  });
@@ -213,7 +230,7 @@ var HttpClient = class {
213
230
  return;
214
231
  }
215
232
  if (!body.data.accessToken || !body.data.refreshToken) {
216
- throw new IQAuthError("TOKEN_INVALID", "Refresh response did not include a token pair");
233
+ throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
217
234
  }
218
235
  const tokens = {
219
236
  accessToken: body.data.accessToken,
@@ -231,7 +248,7 @@ var HttpClient = class {
231
248
  return this.requestWithRetry(method, path, body, options, false);
232
249
  }
233
250
  async requestWithRetry(method, path, body, options, hasRetried) {
234
- if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
251
+ if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
235
252
  await this.attemptRefresh();
236
253
  }
237
254
  const url = `${this.config.baseUrl}${path}`;
@@ -459,6 +476,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
459
476
  "iqvalidate"
460
477
  ];
461
478
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
479
+ function classifyJoseError(err) {
480
+ if (err instanceof import_jose.errors.JWTExpired) {
481
+ return { code: "token_expired", message: "Token has expired" };
482
+ }
483
+ if (err instanceof import_jose.errors.JOSEError) {
484
+ return { code: "token_invalid", message: err.message };
485
+ }
486
+ if (err instanceof Error) {
487
+ return { code: "token_invalid", message: err.message };
488
+ }
489
+ return { code: "token_invalid", message: "Token verification failed" };
490
+ }
462
491
  function decodeProtectedHeader(token) {
463
492
  const parts = token.split(".");
464
493
  if (parts.length < 2) return null;
@@ -495,11 +524,11 @@ var TokensModule = class {
495
524
  async verify(token, options = {}) {
496
525
  const header = decodeProtectedHeader(token);
497
526
  if (!header) {
498
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
527
+ throw new IQAuthError("token_invalid", "Unable to decode token");
499
528
  }
500
529
  const kid = header.kid;
501
530
  if (!kid) {
502
- throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
531
+ throw new IQAuthError("token_invalid", "Token missing kid header");
503
532
  }
504
533
  let cache = await this.ensureCache();
505
534
  if (!cache.byKid.has(kid)) {
@@ -507,7 +536,7 @@ var TokensModule = class {
507
536
  cache = await this.ensureCache();
508
537
  }
509
538
  if (!cache.byKid.has(kid)) {
510
- throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
539
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
511
540
  }
512
541
  const issuer = options.issuer ?? this.defaultIssuer;
513
542
  const audience = options.audience ?? this.defaultAudience;
@@ -523,16 +552,8 @@ var TokensModule = class {
523
552
  const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
524
553
  return payload;
525
554
  } catch (err) {
526
- if (err instanceof import_jose.errors.JWTExpired) {
527
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
528
- }
529
- if (err instanceof import_jose.errors.JOSEError) {
530
- throw new IQAuthError("TOKEN_INVALID", err.message);
531
- }
532
- if (err instanceof Error) {
533
- throw new IQAuthError("TOKEN_INVALID", err.message);
534
- }
535
- throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
555
+ const classified = classifyJoseError(err);
556
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
536
557
  }
537
558
  }
538
559
  /**
@@ -574,7 +595,7 @@ var TokensModule = class {
574
595
  getClaims(token) {
575
596
  const claims = this.decode(token);
576
597
  if (!claims) {
577
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
598
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
578
599
  }
579
600
  return claims;
580
601
  }
@@ -584,7 +605,7 @@ var TokensModule = class {
584
605
  }
585
606
  await this.refreshJwks();
586
607
  if (!this.jwksCache) {
587
- throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
608
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
588
609
  }
589
610
  return this.jwksCache;
590
611
  }
@@ -594,22 +615,38 @@ var TokensModule = class {
594
615
  }
595
616
  this.inFlightRefresh = (async () => {
596
617
  try {
597
- const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
618
+ let res;
619
+ try {
620
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
621
+ } catch (err) {
622
+ throw new IQAuthError(
623
+ "network",
624
+ err instanceof Error ? err.message : "JWKS fetch network error",
625
+ void 0,
626
+ err
627
+ );
628
+ }
598
629
  if (!res.ok) {
599
630
  throw new IQAuthError(
600
- "INTERNAL_ERROR",
601
- `Failed to fetch JWKS: ${res.status}`
631
+ "jwks_fetch_failed",
632
+ `Failed to fetch JWKS: ${res.status}`,
633
+ res.status
602
634
  );
603
635
  }
604
636
  let jwks;
605
637
  try {
606
638
  jwks = await res.json();
607
- } catch {
608
- throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
639
+ } catch (err) {
640
+ throw new IQAuthError(
641
+ "jwks_fetch_failed",
642
+ "Malformed JWKS response: invalid JSON",
643
+ res.status,
644
+ err
645
+ );
609
646
  }
610
647
  if (!jwks || !Array.isArray(jwks.keys)) {
611
648
  throw new IQAuthError(
612
- "INTERNAL_ERROR",
649
+ "jwks_fetch_failed",
613
650
  "Malformed JWKS response: expected { keys: [...] }"
614
651
  );
615
652
  }
@@ -617,7 +654,7 @@ var TokensModule = class {
617
654
  for (const key of jwks.keys) {
618
655
  if (!key || typeof key.kid !== "string" || typeof key.n !== "string" && typeof key.x !== "string" || key.kty === "RSA" && (typeof key.n !== "string" || typeof key.e !== "string")) {
619
656
  throw new IQAuthError(
620
- "INTERNAL_ERROR",
657
+ "jwks_fetch_failed",
621
658
  "Malformed JWKS response: key missing required fields"
622
659
  );
623
660
  }
@@ -635,6 +672,19 @@ var TokensModule = class {
635
672
  clearCache() {
636
673
  this.jwksCache = null;
637
674
  }
675
+ /**
676
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
677
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
678
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
679
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
680
+ */
681
+ async prewarm() {
682
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
683
+ try {
684
+ await this.refreshJwks();
685
+ } catch {
686
+ }
687
+ }
638
688
  };
639
689
 
640
690
  // src/modules/sessions.ts
@@ -958,14 +1008,14 @@ var OidcModule = class {
958
1008
  */
959
1009
  async handleCallback(params) {
960
1010
  if (!params.state) {
961
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing state parameter");
1011
+ throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
962
1012
  }
963
1013
  if (!params.code) {
964
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing code parameter");
1014
+ throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
965
1015
  }
966
1016
  const stored = await this.stateStore.get(params.state);
967
1017
  if (!stored) {
968
- throw new IQAuthError("VALIDATION_ERROR", "Unknown or expired OIDC state");
1018
+ throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
969
1019
  }
970
1020
  let tokens;
971
1021
  try {
@@ -983,7 +1033,7 @@ var OidcModule = class {
983
1033
  if (tokens.id_token) {
984
1034
  if (!this.tokensModule) {
985
1035
  throw new IQAuthError(
986
- "INTERNAL_ERROR",
1036
+ "config_invalid",
987
1037
  "OIDC handleCallback received an id_token but no TokensModule is configured for verification"
988
1038
  );
989
1039
  }
@@ -994,7 +1044,7 @@ var OidcModule = class {
994
1044
  const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
995
1045
  if (!tokenNonce || tokenNonce !== stored.nonce) {
996
1046
  throw new IQAuthError(
997
- "TOKEN_INVALID",
1047
+ "token_invalid",
998
1048
  "OIDC id_token nonce did not match the stored value"
999
1049
  );
1000
1050
  }
@@ -1195,6 +1245,9 @@ var AppsModule = class {
1195
1245
  * @remarks Wraps GET /api/v1/apps/:appKey
1196
1246
  */
1197
1247
  async get(appKey) {
1248
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1249
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1250
+ }
1198
1251
  return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
1199
1252
  }
1200
1253
  /**
@@ -1214,6 +1267,16 @@ var AppsModule = class {
1214
1267
  401
1215
1268
  );
1216
1269
  }
1270
+ if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
1271
+ throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
1272
+ }
1273
+ if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
1274
+ throw new IQAuthError(
1275
+ "ENVIRONMENT_REQUIRED",
1276
+ "manifest.environment is required and must be 'production', 'staging', or 'development'. This guards against a dev workstation silently overwriting a production app's permission tree.",
1277
+ 400
1278
+ );
1279
+ }
1217
1280
  return this.http.request("POST", "/api/v1/apps/sync", manifest);
1218
1281
  }
1219
1282
  /**
@@ -1223,11 +1286,14 @@ var AppsModule = class {
1223
1286
  * @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
1224
1287
  */
1225
1288
  async isRegistered(appKey) {
1289
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1290
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
1291
+ }
1226
1292
  try {
1227
1293
  await this.get(appKey);
1228
1294
  return true;
1229
1295
  } catch (err) {
1230
- if (err.code === "NOT_FOUND" || err.status === 404) {
1296
+ if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
1231
1297
  return false;
1232
1298
  }
1233
1299
  throw err;
@@ -1264,6 +1330,20 @@ var RolesModule = class {
1264
1330
  };
1265
1331
 
1266
1332
  // src/modules/permissionGroups.ts
1333
+ function assertAppKey(appKey, callsite) {
1334
+ if (typeof appKey !== "string" || appKey.trim() === "") {
1335
+ throw new IQAuthError(
1336
+ "VALIDATION_ERROR",
1337
+ `appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
1338
+ 400
1339
+ );
1340
+ }
1341
+ }
1342
+ function assertNodeKey(nodeKey, callsite) {
1343
+ if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
1344
+ throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
1345
+ }
1346
+ }
1267
1347
  var PermissionGroupsModule = class {
1268
1348
  constructor(http) {
1269
1349
  this.http = http;
@@ -1284,7 +1364,14 @@ var PermissionGroupsModule = class {
1284
1364
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
1285
1365
  }
1286
1366
  async addPermission(tenantId, groupId, data) {
1287
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, data);
1367
+ assertAppKey(data?.appKey, "permissionGroups.addPermission");
1368
+ assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
1369
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
1370
+ appKey: data.appKey,
1371
+ nodeKey: data.nodeKey,
1372
+ effect: data.effect,
1373
+ weight: data.weight
1374
+ });
1288
1375
  }
1289
1376
  async removePermission(tenantId, groupId, permissionId) {
1290
1377
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
@@ -1308,21 +1395,51 @@ var PermissionGroupsModule = class {
1308
1395
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
1309
1396
  }
1310
1397
  async addUserOverride(tenantId, userId, data) {
1311
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, data);
1398
+ assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
1399
+ assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
1400
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
1401
+ appKey: data.appKey,
1402
+ nodeKey: data.nodeKey,
1403
+ effect: data.effect,
1404
+ weight: data.weight,
1405
+ expiresAt: data.expiresAt
1406
+ });
1312
1407
  }
1313
1408
  async removeUserOverride(tenantId, userId, overrideId) {
1314
1409
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
1315
1410
  }
1411
+ /**
1412
+ * Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
1413
+ * longer accepted at the SDK boundary; pass it as `appKey` instead. The
1414
+ * server still accepts `product=` from raw HTTP callers during the
1415
+ * deprecation window, but the SDK will not silently translate it.
1416
+ */
1316
1417
  async getEffectivePermissions(tenantId, userId, params) {
1317
- const query = new URLSearchParams();
1318
- if (params.product) query.set("product", params.product);
1319
- if (params.appKey) query.set("appKey", params.appKey);
1320
- const qs = query.toString();
1321
- return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
1418
+ assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
1419
+ const qs = new URLSearchParams({ appKey: params.appKey }).toString();
1420
+ return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
1322
1421
  }
1323
1422
  async checkPermission(tenantId, userId, appKey, nodeKey) {
1423
+ assertAppKey(appKey, "permissionGroups.checkPermission");
1424
+ assertNodeKey(nodeKey, "permissionGroups.checkPermission");
1324
1425
  return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
1325
1426
  }
1427
+ /**
1428
+ * Task #130 — every entry in `checks` must include a non-empty `appKey`
1429
+ * AND `nodeKey`. The SDK validates the whole batch before sending so a
1430
+ * single misconfigured entry can't slip through and silently report
1431
+ * `allowed: false` from the server's per-entry validation branch.
1432
+ */
1433
+ async batchCheckPermissions(tenantId, userId, checks) {
1434
+ if (!Array.isArray(checks) || checks.length === 0) {
1435
+ throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
1436
+ }
1437
+ checks.forEach((c, i) => {
1438
+ assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
1439
+ assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
1440
+ });
1441
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
1442
+ }
1326
1443
  };
1327
1444
 
1328
1445
  // src/modules/apiKeys.ts
@@ -1747,6 +1864,10 @@ var IQAuthClient = class _IQAuthClient {
1747
1864
  this._refreshToken = tokens.refreshToken;
1748
1865
  },
1749
1866
  autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
1867
+ // `'app-state'` is mobile-only — on any other environment we treat it
1868
+ // as the default `true` (proactive refresh ON). Only the mobile client
1869
+ // disables proactive refresh and replaces it with an AppState listener.
1870
+ proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
1750
1871
  onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
1751
1872
  sessionHeaderName: config.sessionHeaderName,
1752
1873
  sessionHeaderValue: config.sessionHeaderValue,
@@ -1787,6 +1908,13 @@ var IQAuthClient = class _IQAuthClient {
1787
1908
  static forServer(config) {
1788
1909
  return new _IQAuthClient({ ...config, environment: "server" });
1789
1910
  }
1911
+ /**
1912
+ * Construct a mobile-environment client. NOTE: this constructor does NOT
1913
+ * subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
1914
+ * is passed — it only disables the per-request proactive refresh. Use
1915
+ * `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
1916
+ * AppState-driven refresh behavior (recommended for Expo / React Native).
1917
+ */
1790
1918
  static forMobile(config) {
1791
1919
  return new _IQAuthClient({ ...config, environment: "mobile" });
1792
1920
  }
@@ -1803,6 +1931,18 @@ var IQAuthClient = class _IQAuthClient {
1803
1931
  getRefreshToken() {
1804
1932
  return this._refreshToken;
1805
1933
  }
1934
+ /**
1935
+ * Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
1936
+ * refresh round-trip on the request hot path doesn't pay the discovery
1937
+ * fetch latency. Safe to call repeatedly. Errors are swallowed; callers
1938
+ * may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
1939
+ */
1940
+ async prewarm() {
1941
+ await Promise.all([
1942
+ this.tokens.prewarm(),
1943
+ this.oidc.getDiscovery().catch(() => void 0)
1944
+ ]);
1945
+ }
1806
1946
  getCurrentClaims() {
1807
1947
  if (!this._accessToken) return null;
1808
1948
  return this.tokens.decode(this._accessToken);
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  IQAuthClient
3
- } from "./chunk-W3F4JYGP.mjs";
4
- import "./chunk-UNYDG2L4.mjs";
3
+ } from "./chunk-JXQI62A7.mjs";
4
+ import "./chunk-NUO2I65G.mjs";
5
5
  import {
6
6
  ErrorCodes,
7
7
  IQAuthError
8
- } from "./chunk-6I6RM4MN.mjs";
8
+ } from "./chunk-6PJRLRB4.mjs";
9
9
  import "./chunk-Y6FXYEAI.mjs";
10
10
 
11
11
  // src/browser-session.ts