@iqauth/sdk 2.6.4 → 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 (110) 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 +5 -5
  10. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  11. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  12. package/dist/chunk-GLXSIGVS.mjs +66 -0
  13. package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
  14. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  15. package/dist/{chunk-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
  16. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  17. package/dist/chunk-PMAFENVI.mjs +229 -0
  18. package/dist/chunk-RR2MGPTK.mjs +2724 -0
  19. package/dist/{chunk-XAWYUPMO.mjs → chunk-RTJAIBXY.mjs} +220 -20
  20. package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
  21. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  22. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  23. package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
  24. package/dist/cli/index.js +2 -2
  25. package/dist/cli/index.mjs +2 -2
  26. package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
  27. package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
  28. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  29. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  30. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  31. package/dist/{express-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
  32. package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
  33. package/dist/express.d.mts +7 -6
  34. package/dist/express.d.ts +7 -6
  35. package/dist/express.js +349 -52
  36. package/dist/express.mjs +39 -12
  37. package/dist/fastify.d.mts +2 -0
  38. package/dist/fastify.d.ts +2 -0
  39. package/dist/fastify.js +332 -52
  40. package/dist/fastify.mjs +23 -8
  41. package/dist/hono.d.mts +2 -0
  42. package/dist/hono.d.ts +2 -0
  43. package/dist/hono.js +329 -52
  44. package/dist/hono.mjs +20 -8
  45. package/dist/index-5KSZEnDe.d.ts +1626 -0
  46. package/dist/index-CKoZHAoc.d.mts +1626 -0
  47. package/dist/index.d.mts +56 -8
  48. package/dist/index.d.ts +56 -8
  49. package/dist/index.js +565 -69
  50. package/dist/index.mjs +29 -9
  51. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  52. package/dist/locales.d.mts +1 -1
  53. package/dist/locales.d.ts +1 -1
  54. package/dist/mobile.d.mts +77 -7
  55. package/dist/mobile.d.ts +77 -7
  56. package/dist/mobile.js +276 -41
  57. package/dist/mobile.mjs +98 -3
  58. package/dist/next.d.mts +2 -1
  59. package/dist/next.d.ts +2 -1
  60. package/dist/next.js +391 -201
  61. package/dist/next.mjs +22 -7
  62. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
  63. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
  64. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  65. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  66. package/dist/react-permissions.d.mts +52 -0
  67. package/dist/react-permissions.d.ts +52 -0
  68. package/dist/react-permissions.js +239 -0
  69. package/dist/react-permissions.mjs +97 -0
  70. package/dist/react.d.mts +9 -1624
  71. package/dist/react.d.ts +9 -1624
  72. package/dist/react.js +313 -33
  73. package/dist/react.mjs +58 -2632
  74. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  75. package/dist/server/handlers.d.mts +148 -3
  76. package/dist/server/handlers.d.ts +148 -3
  77. package/dist/server/handlers.js +410 -11
  78. package/dist/server/handlers.mjs +12 -3
  79. package/dist/server.d.mts +151 -8
  80. package/dist/server.d.ts +151 -8
  81. package/dist/server.js +406 -50
  82. package/dist/server.mjs +93 -11
  83. package/dist/service.d.mts +4 -4
  84. package/dist/service.d.ts +4 -4
  85. package/dist/service.js +181 -41
  86. package/dist/service.mjs +3 -3
  87. package/dist/{signIn-OCr88Zf8.d.ts → signIn-BLFnz8SV.d.ts} +78 -3
  88. package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
  89. package/dist/{signIn-CiIBTJIh.d.mts → signIn-T-CZ6t6r.d.mts} +78 -3
  90. package/dist/test.mjs +3 -3
  91. package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
  92. package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
  93. package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
  94. package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
  95. package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
  96. package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
  97. package/dist/webhooks.d.mts +100 -17
  98. package/dist/webhooks.d.ts +100 -17
  99. package/dist/webhooks.js +164 -15
  100. package/dist/webhooks.mjs +7 -1
  101. package/dist/ws.d.mts +2 -2
  102. package/dist/ws.d.ts +2 -2
  103. package/dist/ws.js +80 -30
  104. package/dist/ws.mjs +4 -4
  105. package/docs/error-handling.md +101 -0
  106. package/docs/guides/effective-permissions.md +171 -0
  107. package/package.json +13 -3
  108. package/dist/chunk-UKZLOHZG.mjs +0 -83
  109. package/dist/errors-CDdl24MP.d.mts +0 -52
  110. package/dist/errors-CDdl24MP.d.ts +0 -52
@@ -0,0 +1,66 @@
1
+ // src/permissions/wildcard.ts
2
+ var SUFFIX = ".*";
3
+ function wildcardPrefix(pattern) {
4
+ return pattern.slice(0, -SUFFIX.length);
5
+ }
6
+ function hasPermission(set, id) {
7
+ if (!id) return false;
8
+ if (!set) return false;
9
+ if (id === "*") {
10
+ for (const entry of set) if (entry === "*") return true;
11
+ return false;
12
+ }
13
+ const queryIsWildcard = id.endsWith(SUFFIX);
14
+ const queryPrefix = queryIsWildcard ? wildcardPrefix(id) : null;
15
+ for (const entry of set) {
16
+ if (!entry) continue;
17
+ if (entry === "*") return true;
18
+ if (entry === id) return true;
19
+ if (entry.endsWith(SUFFIX)) {
20
+ const prefix = wildcardPrefix(entry);
21
+ if (!queryIsWildcard) {
22
+ if (id === prefix) return true;
23
+ if (id.startsWith(prefix + ".")) return true;
24
+ } else {
25
+ if (queryPrefix === prefix) return true;
26
+ if (queryPrefix !== null && queryPrefix.startsWith(prefix + ".")) return true;
27
+ }
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+ function expandPermissions(set) {
33
+ if (!set) return [];
34
+ const seen = /* @__PURE__ */ new Set();
35
+ for (const raw of set) {
36
+ if (typeof raw !== "string" || raw.length === 0) continue;
37
+ seen.add(raw);
38
+ }
39
+ if (seen.has("*")) return ["*"];
40
+ const wildcards = [];
41
+ for (const entry of seen) if (entry.endsWith(SUFFIX)) wildcards.push(entry);
42
+ const out = [];
43
+ for (const entry of seen) {
44
+ let covered = false;
45
+ for (const w of wildcards) {
46
+ if (w === entry) continue;
47
+ const prefix = wildcardPrefix(w);
48
+ if (entry === prefix) {
49
+ covered = true;
50
+ break;
51
+ }
52
+ if (entry.startsWith(prefix + ".")) {
53
+ covered = true;
54
+ break;
55
+ }
56
+ }
57
+ if (!covered) out.push(entry);
58
+ }
59
+ out.sort();
60
+ return out;
61
+ }
62
+
63
+ export {
64
+ hasPermission,
65
+ expandPermissions
66
+ };
@@ -91,7 +91,7 @@ async function buildSignInUrl(manager, opts = {}) {
91
91
  returnTo,
92
92
  createdAt: Date.now()
93
93
  });
94
- const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.issuerUrl);
94
+ const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.hostedIssuerUrl);
95
95
  url.searchParams.set("response_type", "code");
96
96
  url.searchParams.set("app", manager.appKey);
97
97
  url.searchParams.set("publishable_key", manager.publishableKey.raw);
@@ -106,33 +106,50 @@ async function buildSignInUrl(manager, opts = {}) {
106
106
  return url.toString();
107
107
  }
108
108
  async function redirectToSignIn(manager, opts = {}) {
109
- const url = await buildSignInUrl(manager, opts);
110
- if (typeof window === "undefined") {
111
- throw new Error("redirectToSignIn requires a browser environment");
109
+ const t0 = Date.now();
110
+ let ok = false;
111
+ let code;
112
+ try {
113
+ const url = await buildSignInUrl(manager, opts);
114
+ if (typeof window === "undefined") {
115
+ code = "NO_WINDOW";
116
+ throw new Error("redirectToSignIn requires a browser environment");
117
+ }
118
+ ok = true;
119
+ manager.recordTiming("signIn", Date.now() - t0, true);
120
+ window.location.assign(url);
121
+ } catch (err) {
122
+ if (!ok) manager.recordTiming("signIn", Date.now() - t0, false, code ?? (err instanceof Error ? err.message : "ERROR"));
123
+ throw err;
112
124
  }
113
- window.location.assign(url);
114
125
  }
115
126
  async function signIn(manager, opts = {}) {
116
127
  return redirectToSignIn(manager, opts);
117
128
  }
118
129
  async function handleAuthCallback(manager, options = {}) {
130
+ const t0 = Date.now();
131
+ const emit = (ok, code2) => manager.recordTiming("signIn", Date.now() - t0, ok, code2);
119
132
  const url = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
120
133
  const code = url.searchParams.get("code");
121
134
  const state = url.searchParams.get("state");
122
135
  const errorParam = url.searchParams.get("error");
123
136
  if (errorParam) {
137
+ emit(false, errorParam);
124
138
  return { ok: false, returnTo: "/", error: errorParam };
125
139
  }
126
140
  if (!code || !state) {
141
+ emit(false, "missing_code_or_state");
127
142
  return { ok: false, returnTo: "/", error: "missing_code_or_state" };
128
143
  }
129
144
  const record = loadPkce(state);
130
145
  if (!record) {
146
+ emit(false, "unknown_state");
131
147
  return { ok: false, returnTo: "/", error: "unknown_state" };
132
148
  }
133
149
  clearPkce(state);
134
150
  const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
135
151
  if (!fetchImpl) {
152
+ emit(false, "no_fetch");
136
153
  return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
137
154
  }
138
155
  const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
@@ -151,10 +168,12 @@ async function handleAuthCallback(manager, options = {}) {
151
168
  const body = await res.json().catch(() => ({}));
152
169
  if (!res.ok) {
153
170
  const desc = body.error_description ?? body.error ?? "token_exchange_failed";
171
+ emit(false, desc);
154
172
  return { ok: false, returnTo: record.returnTo, error: desc };
155
173
  }
156
174
  const tokens = body;
157
175
  if (!tokens.access_token) {
176
+ emit(false, "missing_access_token");
158
177
  return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
159
178
  }
160
179
  if (tokens.refresh_token) {
@@ -162,21 +181,24 @@ async function handleAuthCallback(manager, options = {}) {
162
181
  setCookie(cookieName, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
163
182
  }
164
183
  manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
184
+ emit(true);
165
185
  return { ok: true, returnTo: record.returnTo };
166
186
  }
167
187
  async function signOut(manager, opts = {}) {
168
188
  if (!opts.localOnly) {
169
189
  const issuer = manager.issuerUrl.replace(/\/$/, "");
190
+ const idempotency = manager.getIdempotencyToken();
170
191
  try {
171
192
  const url = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
172
- await manager.fetch(url, { method: "POST" }).catch(() => void 0);
193
+ await manager.fetch(url, { method: "POST", headers: { "X-IQAuth-Idempotency": idempotency } }).catch(() => void 0);
173
194
  } catch {
174
195
  }
175
196
  if (opts.endSsoSession !== false) {
176
197
  try {
177
198
  await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
178
199
  method: "POST",
179
- credentials: "include"
200
+ credentials: "include",
201
+ headers: { "X-IQAuth-Idempotency": idempotency }
180
202
  }).catch(() => void 0);
181
203
  } catch {
182
204
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IQAuthError
3
- } from "./chunk-6I6RM4MN.mjs";
3
+ } from "./chunk-6PJRLRB4.mjs";
4
4
  import {
5
5
  __require
6
6
  } from "./chunk-Y6FXYEAI.mjs";
@@ -64,14 +64,14 @@ function assertPublishableKey(raw, opts) {
64
64
  const ctx = opts?.context ? `${opts.context}: ` : "";
65
65
  if (typeof raw !== "string" || raw.length === 0) {
66
66
  throw new IQAuthError(
67
- "CONFIG_INVALID",
67
+ "config_invalid",
68
68
  `${ctx}IQAuth publishable key is missing. Set IQAUTH_PUBLISHABLE_KEY (or pass publishableKey) to a pk_test_\u2026 or pk_live_\u2026 value from the IQAuth admin console.`
69
69
  );
70
70
  }
71
71
  const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
72
72
  if (!shapeMatch) {
73
73
  throw new IQAuthError(
74
- "CONFIG_INVALID",
74
+ "config_invalid",
75
75
  `${ctx}IQAuth publishable key is malformed (got ${raw.slice(0, 12)}\u2026). Expected pk_test_\u2026 or pk_live_\u2026; regenerate the key from the IQAuth admin console.`
76
76
  );
77
77
  }
@@ -80,19 +80,19 @@ function assertPublishableKey(raw, opts) {
80
80
  decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
81
81
  } catch {
82
82
  throw new IQAuthError(
83
- "CONFIG_INVALID",
83
+ "config_invalid",
84
84
  `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
85
85
  );
86
86
  }
87
87
  if (!isPublishableKeyPayload(decoded)) {
88
88
  throw new IQAuthError(
89
- "CONFIG_INVALID",
89
+ "config_invalid",
90
90
  `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
91
91
  );
92
92
  }
93
93
  if (!isValidIssuerUrl(decoded.iss)) {
94
94
  throw new IQAuthError(
95
- "CONFIG_INVALID",
95
+ "config_invalid",
96
96
  `${ctx}IQAuth publishable key encodes an invalid issuer (iss=${JSON.stringify(decoded.iss)}). Expected a fully-qualified URL like "https://auth.example.com" (scheme required). Regenerate the key from the IQAuth admin console \u2014 the new key will encode a valid issuer URL.`
97
97
  );
98
98
  }
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  TokensModule
3
- } from "./chunk-UNYDG2L4.mjs";
3
+ } from "./chunk-NUO2I65G.mjs";
4
4
  import {
5
5
  IQAuthError
6
- } from "./chunk-6I6RM4MN.mjs";
6
+ } from "./chunk-6PJRLRB4.mjs";
7
7
 
8
8
  // src/modules/auth.ts
9
9
  function parseLoginResponse(data, browserSessionMode) {
@@ -484,14 +484,14 @@ var OidcModule = class {
484
484
  */
485
485
  async handleCallback(params) {
486
486
  if (!params.state) {
487
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing state parameter");
487
+ throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
488
488
  }
489
489
  if (!params.code) {
490
- throw new IQAuthError("VALIDATION_ERROR", "OIDC callback missing code parameter");
490
+ throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
491
491
  }
492
492
  const stored = await this.stateStore.get(params.state);
493
493
  if (!stored) {
494
- throw new IQAuthError("VALIDATION_ERROR", "Unknown or expired OIDC state");
494
+ throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
495
495
  }
496
496
  let tokens;
497
497
  try {
@@ -509,7 +509,7 @@ var OidcModule = class {
509
509
  if (tokens.id_token) {
510
510
  if (!this.tokensModule) {
511
511
  throw new IQAuthError(
512
- "INTERNAL_ERROR",
512
+ "config_invalid",
513
513
  "OIDC handleCallback received an id_token but no TokensModule is configured for verification"
514
514
  );
515
515
  }
@@ -520,7 +520,7 @@ var OidcModule = class {
520
520
  const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
521
521
  if (!tokenNonce || tokenNonce !== stored.nonce) {
522
522
  throw new IQAuthError(
523
- "TOKEN_INVALID",
523
+ "token_invalid",
524
524
  "OIDC id_token nonce did not match the stored value"
525
525
  );
526
526
  }
@@ -721,6 +721,9 @@ var AppsModule = class {
721
721
  * @remarks Wraps GET /api/v1/apps/:appKey
722
722
  */
723
723
  async get(appKey) {
724
+ if (typeof appKey !== "string" || appKey.trim() === "") {
725
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
726
+ }
724
727
  return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
725
728
  }
726
729
  /**
@@ -740,6 +743,16 @@ var AppsModule = class {
740
743
  401
741
744
  );
742
745
  }
746
+ if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
747
+ throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
748
+ }
749
+ if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
750
+ throw new IQAuthError(
751
+ "ENVIRONMENT_REQUIRED",
752
+ "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.",
753
+ 400
754
+ );
755
+ }
743
756
  return this.http.request("POST", "/api/v1/apps/sync", manifest);
744
757
  }
745
758
  /**
@@ -749,11 +762,14 @@ var AppsModule = class {
749
762
  * @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
750
763
  */
751
764
  async isRegistered(appKey) {
765
+ if (typeof appKey !== "string" || appKey.trim() === "") {
766
+ throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
767
+ }
752
768
  try {
753
769
  await this.get(appKey);
754
770
  return true;
755
771
  } catch (err) {
756
- if (err.code === "NOT_FOUND" || err.status === 404) {
772
+ if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
757
773
  return false;
758
774
  }
759
775
  throw err;
@@ -790,6 +806,20 @@ var RolesModule = class {
790
806
  };
791
807
 
792
808
  // src/modules/permissionGroups.ts
809
+ function assertAppKey(appKey, callsite) {
810
+ if (typeof appKey !== "string" || appKey.trim() === "") {
811
+ throw new IQAuthError(
812
+ "VALIDATION_ERROR",
813
+ `appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
814
+ 400
815
+ );
816
+ }
817
+ }
818
+ function assertNodeKey(nodeKey, callsite) {
819
+ if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
820
+ throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
821
+ }
822
+ }
793
823
  var PermissionGroupsModule = class {
794
824
  constructor(http) {
795
825
  this.http = http;
@@ -810,7 +840,14 @@ var PermissionGroupsModule = class {
810
840
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
811
841
  }
812
842
  async addPermission(tenantId, groupId, data) {
813
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, data);
843
+ assertAppKey(data?.appKey, "permissionGroups.addPermission");
844
+ assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
845
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
846
+ appKey: data.appKey,
847
+ nodeKey: data.nodeKey,
848
+ effect: data.effect,
849
+ weight: data.weight
850
+ });
814
851
  }
815
852
  async removePermission(tenantId, groupId, permissionId) {
816
853
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
@@ -834,21 +871,51 @@ var PermissionGroupsModule = class {
834
871
  return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
835
872
  }
836
873
  async addUserOverride(tenantId, userId, data) {
837
- return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, data);
874
+ assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
875
+ assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
876
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
877
+ appKey: data.appKey,
878
+ nodeKey: data.nodeKey,
879
+ effect: data.effect,
880
+ weight: data.weight,
881
+ expiresAt: data.expiresAt
882
+ });
838
883
  }
839
884
  async removeUserOverride(tenantId, userId, overrideId) {
840
885
  return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
841
886
  }
887
+ /**
888
+ * Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
889
+ * longer accepted at the SDK boundary; pass it as `appKey` instead. The
890
+ * server still accepts `product=` from raw HTTP callers during the
891
+ * deprecation window, but the SDK will not silently translate it.
892
+ */
842
893
  async getEffectivePermissions(tenantId, userId, params) {
843
- const query = new URLSearchParams();
844
- if (params.product) query.set("product", params.product);
845
- if (params.appKey) query.set("appKey", params.appKey);
846
- const qs = query.toString();
847
- return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
894
+ assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
895
+ const qs = new URLSearchParams({ appKey: params.appKey }).toString();
896
+ return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
848
897
  }
849
898
  async checkPermission(tenantId, userId, appKey, nodeKey) {
899
+ assertAppKey(appKey, "permissionGroups.checkPermission");
900
+ assertNodeKey(nodeKey, "permissionGroups.checkPermission");
850
901
  return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
851
902
  }
903
+ /**
904
+ * Task #130 — every entry in `checks` must include a non-empty `appKey`
905
+ * AND `nodeKey`. The SDK validates the whole batch before sending so a
906
+ * single misconfigured entry can't slip through and silently report
907
+ * `allowed: false` from the server's per-entry validation branch.
908
+ */
909
+ async batchCheckPermissions(tenantId, userId, checks) {
910
+ if (!Array.isArray(checks) || checks.length === 0) {
911
+ throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
912
+ }
913
+ checks.forEach((c, i) => {
914
+ assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
915
+ assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
916
+ });
917
+ return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
918
+ }
852
919
  };
853
920
 
854
921
  // src/modules/apiKeys.ts
@@ -1365,7 +1432,7 @@ var HttpClient = class {
1365
1432
  headers: this.buildHeaders(),
1366
1433
  ...this.isBrowserSession() ? { credentials: "include" } : (() => {
1367
1434
  const refreshToken = this.config.getRefreshToken();
1368
- if (!refreshToken) throw new IQAuthError("TOKEN_INVALID", "No refresh token available");
1435
+ if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
1369
1436
  return { body: JSON.stringify({ refreshToken }) };
1370
1437
  })()
1371
1438
  });
@@ -1382,7 +1449,7 @@ var HttpClient = class {
1382
1449
  return;
1383
1450
  }
1384
1451
  if (!body.data.accessToken || !body.data.refreshToken) {
1385
- throw new IQAuthError("TOKEN_INVALID", "Refresh response did not include a token pair");
1452
+ throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
1386
1453
  }
1387
1454
  const tokens = {
1388
1455
  accessToken: body.data.accessToken,
@@ -1400,7 +1467,7 @@ var HttpClient = class {
1400
1467
  return this.requestWithRetry(method, path, body, options, false);
1401
1468
  }
1402
1469
  async requestWithRetry(method, path, body, options, hasRetried) {
1403
- if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
1470
+ if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
1404
1471
  await this.attemptRefresh();
1405
1472
  }
1406
1473
  const url = `${this.config.baseUrl}${path}`;
@@ -1475,6 +1542,10 @@ var IQAuthClient = class _IQAuthClient {
1475
1542
  this._refreshToken = tokens.refreshToken;
1476
1543
  },
1477
1544
  autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
1545
+ // `'app-state'` is mobile-only — on any other environment we treat it
1546
+ // as the default `true` (proactive refresh ON). Only the mobile client
1547
+ // disables proactive refresh and replaces it with an AppState listener.
1548
+ proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
1478
1549
  onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
1479
1550
  sessionHeaderName: config.sessionHeaderName,
1480
1551
  sessionHeaderValue: config.sessionHeaderValue,
@@ -1515,6 +1586,13 @@ var IQAuthClient = class _IQAuthClient {
1515
1586
  static forServer(config) {
1516
1587
  return new _IQAuthClient({ ...config, environment: "server" });
1517
1588
  }
1589
+ /**
1590
+ * Construct a mobile-environment client. NOTE: this constructor does NOT
1591
+ * subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
1592
+ * is passed — it only disables the per-request proactive refresh. Use
1593
+ * `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
1594
+ * AppState-driven refresh behavior (recommended for Expo / React Native).
1595
+ */
1518
1596
  static forMobile(config) {
1519
1597
  return new _IQAuthClient({ ...config, environment: "mobile" });
1520
1598
  }
@@ -1531,6 +1609,18 @@ var IQAuthClient = class _IQAuthClient {
1531
1609
  getRefreshToken() {
1532
1610
  return this._refreshToken;
1533
1611
  }
1612
+ /**
1613
+ * Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
1614
+ * refresh round-trip on the request hot path doesn't pay the discovery
1615
+ * fetch latency. Safe to call repeatedly. Errors are swallowed; callers
1616
+ * may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
1617
+ */
1618
+ async prewarm() {
1619
+ await Promise.all([
1620
+ this.tokens.prewarm(),
1621
+ this.oidc.getDiscovery().catch(() => void 0)
1622
+ ]);
1623
+ }
1534
1624
  getCurrentClaims() {
1535
1625
  if (!this._accessToken) return null;
1536
1626
  return this.tokens.decode(this._accessToken);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IQAuthError
3
- } from "./chunk-6I6RM4MN.mjs";
3
+ } from "./chunk-6PJRLRB4.mjs";
4
4
  import {
5
5
  __require
6
6
  } from "./chunk-Y6FXYEAI.mjs";
@@ -23,6 +23,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
23
23
  "iqvalidate"
24
24
  ];
25
25
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
26
+ function classifyJoseError(err) {
27
+ if (err instanceof joseErrors.JWTExpired) {
28
+ return { code: "token_expired", message: "Token has expired" };
29
+ }
30
+ if (err instanceof joseErrors.JOSEError) {
31
+ return { code: "token_invalid", message: err.message };
32
+ }
33
+ if (err instanceof Error) {
34
+ return { code: "token_invalid", message: err.message };
35
+ }
36
+ return { code: "token_invalid", message: "Token verification failed" };
37
+ }
26
38
  function decodeProtectedHeader(token) {
27
39
  const parts = token.split(".");
28
40
  if (parts.length < 2) return null;
@@ -59,11 +71,11 @@ var TokensModule = class {
59
71
  async verify(token, options = {}) {
60
72
  const header = decodeProtectedHeader(token);
61
73
  if (!header) {
62
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
74
+ throw new IQAuthError("token_invalid", "Unable to decode token");
63
75
  }
64
76
  const kid = header.kid;
65
77
  if (!kid) {
66
- throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
78
+ throw new IQAuthError("token_invalid", "Token missing kid header");
67
79
  }
68
80
  let cache = await this.ensureCache();
69
81
  if (!cache.byKid.has(kid)) {
@@ -71,7 +83,7 @@ var TokensModule = class {
71
83
  cache = await this.ensureCache();
72
84
  }
73
85
  if (!cache.byKid.has(kid)) {
74
- throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
86
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
75
87
  }
76
88
  const issuer = options.issuer ?? this.defaultIssuer;
77
89
  const audience = options.audience ?? this.defaultAudience;
@@ -87,16 +99,8 @@ var TokensModule = class {
87
99
  const { payload } = await jwtVerify(token, cache.verifier, verifyOptions);
88
100
  return payload;
89
101
  } catch (err) {
90
- if (err instanceof joseErrors.JWTExpired) {
91
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
92
- }
93
- if (err instanceof joseErrors.JOSEError) {
94
- throw new IQAuthError("TOKEN_INVALID", err.message);
95
- }
96
- if (err instanceof Error) {
97
- throw new IQAuthError("TOKEN_INVALID", err.message);
98
- }
99
- throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
102
+ const classified = classifyJoseError(err);
103
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
100
104
  }
101
105
  }
102
106
  /**
@@ -138,7 +142,7 @@ var TokensModule = class {
138
142
  getClaims(token) {
139
143
  const claims = this.decode(token);
140
144
  if (!claims) {
141
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
145
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
142
146
  }
143
147
  return claims;
144
148
  }
@@ -148,7 +152,7 @@ var TokensModule = class {
148
152
  }
149
153
  await this.refreshJwks();
150
154
  if (!this.jwksCache) {
151
- throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
155
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
152
156
  }
153
157
  return this.jwksCache;
154
158
  }
@@ -158,22 +162,38 @@ var TokensModule = class {
158
162
  }
159
163
  this.inFlightRefresh = (async () => {
160
164
  try {
161
- const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
165
+ let res;
166
+ try {
167
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
168
+ } catch (err) {
169
+ throw new IQAuthError(
170
+ "network",
171
+ err instanceof Error ? err.message : "JWKS fetch network error",
172
+ void 0,
173
+ err
174
+ );
175
+ }
162
176
  if (!res.ok) {
163
177
  throw new IQAuthError(
164
- "INTERNAL_ERROR",
165
- `Failed to fetch JWKS: ${res.status}`
178
+ "jwks_fetch_failed",
179
+ `Failed to fetch JWKS: ${res.status}`,
180
+ res.status
166
181
  );
167
182
  }
168
183
  let jwks;
169
184
  try {
170
185
  jwks = await res.json();
171
- } catch {
172
- throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
186
+ } catch (err) {
187
+ throw new IQAuthError(
188
+ "jwks_fetch_failed",
189
+ "Malformed JWKS response: invalid JSON",
190
+ res.status,
191
+ err
192
+ );
173
193
  }
174
194
  if (!jwks || !Array.isArray(jwks.keys)) {
175
195
  throw new IQAuthError(
176
- "INTERNAL_ERROR",
196
+ "jwks_fetch_failed",
177
197
  "Malformed JWKS response: expected { keys: [...] }"
178
198
  );
179
199
  }
@@ -181,7 +201,7 @@ var TokensModule = class {
181
201
  for (const key of jwks.keys) {
182
202
  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")) {
183
203
  throw new IQAuthError(
184
- "INTERNAL_ERROR",
204
+ "jwks_fetch_failed",
185
205
  "Malformed JWKS response: key missing required fields"
186
206
  );
187
207
  }
@@ -199,6 +219,19 @@ var TokensModule = class {
199
219
  clearCache() {
200
220
  this.jwksCache = null;
201
221
  }
222
+ /**
223
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
224
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
225
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
226
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
227
+ */
228
+ async prewarm() {
229
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
230
+ try {
231
+ await this.refreshJwks();
232
+ } catch {
233
+ }
234
+ }
202
235
  };
203
236
 
204
237
  export {