@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
@@ -0,0 +1,36 @@
1
+ // src/browser/pkce.ts
2
+ function getCrypto() {
3
+ if (typeof globalThis !== "undefined" && globalThis.crypto) {
4
+ return globalThis.crypto;
5
+ }
6
+ throw new Error("WebCrypto is not available in this environment");
7
+ }
8
+ function base64UrlEncode(bytes) {
9
+ let bin = "";
10
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
11
+ const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
12
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
13
+ }
14
+ function randomUrlSafe(byteLength = 32) {
15
+ const bytes = new Uint8Array(byteLength);
16
+ getCrypto().getRandomValues(bytes);
17
+ return base64UrlEncode(bytes);
18
+ }
19
+ async function s256Challenge(verifier) {
20
+ const data = new TextEncoder().encode(verifier);
21
+ const digest = await getCrypto().subtle.digest("SHA-256", data);
22
+ return base64UrlEncode(new Uint8Array(digest));
23
+ }
24
+ async function createPkcePair() {
25
+ const codeVerifier = randomUrlSafe(32);
26
+ const codeChallenge = await s256Challenge(codeVerifier);
27
+ const state = randomUrlSafe(16);
28
+ const nonce = randomUrlSafe(16);
29
+ return { codeVerifier, codeChallenge, state, nonce };
30
+ }
31
+
32
+ export {
33
+ randomUrlSafe,
34
+ s256Challenge,
35
+ createPkcePair
36
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IQAuthError
3
- } from "./chunk-6I6RM4MN.mjs";
3
+ } from "./chunk-6PJRLRB4.mjs";
4
4
 
5
5
  // src/browser/reverify.ts
6
6
  var PRIOR_SESSION_STORAGE_KEY = "iqauth_prior_admin_session";
@@ -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
+ };
@@ -1,33 +1,6 @@
1
- // src/browser/pkce.ts
2
- function getCrypto() {
3
- if (typeof globalThis !== "undefined" && globalThis.crypto) {
4
- return globalThis.crypto;
5
- }
6
- throw new Error("WebCrypto is not available in this environment");
7
- }
8
- function base64UrlEncode(bytes) {
9
- let bin = "";
10
- for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
11
- const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
12
- return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
13
- }
14
- function randomUrlSafe(byteLength = 32) {
15
- const bytes = new Uint8Array(byteLength);
16
- getCrypto().getRandomValues(bytes);
17
- return base64UrlEncode(bytes);
18
- }
19
- async function s256Challenge(verifier) {
20
- const data = new TextEncoder().encode(verifier);
21
- const digest = await getCrypto().subtle.digest("SHA-256", data);
22
- return base64UrlEncode(new Uint8Array(digest));
23
- }
24
- async function createPkcePair() {
25
- const codeVerifier = randomUrlSafe(32);
26
- const codeChallenge = await s256Challenge(codeVerifier);
27
- const state = randomUrlSafe(16);
28
- const nonce = randomUrlSafe(16);
29
- return { codeVerifier, codeChallenge, state, nonce };
30
- }
1
+ import {
2
+ createPkcePair
3
+ } from "./chunk-C2ZTBOAC.mjs";
31
4
 
32
5
  // src/browser/storage.ts
33
6
  var REFRESH_COOKIE = "iqauth_rt";
@@ -118,7 +91,7 @@ async function buildSignInUrl(manager, opts = {}) {
118
91
  returnTo,
119
92
  createdAt: Date.now()
120
93
  });
121
- 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);
122
95
  url.searchParams.set("response_type", "code");
123
96
  url.searchParams.set("app", manager.appKey);
124
97
  url.searchParams.set("publishable_key", manager.publishableKey.raw);
@@ -133,33 +106,50 @@ async function buildSignInUrl(manager, opts = {}) {
133
106
  return url.toString();
134
107
  }
135
108
  async function redirectToSignIn(manager, opts = {}) {
136
- const url = await buildSignInUrl(manager, opts);
137
- if (typeof window === "undefined") {
138
- 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;
139
124
  }
140
- window.location.assign(url);
141
125
  }
142
126
  async function signIn(manager, opts = {}) {
143
127
  return redirectToSignIn(manager, opts);
144
128
  }
145
129
  async function handleAuthCallback(manager, options = {}) {
130
+ const t0 = Date.now();
131
+ const emit = (ok, code2) => manager.recordTiming("signIn", Date.now() - t0, ok, code2);
146
132
  const url = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
147
133
  const code = url.searchParams.get("code");
148
134
  const state = url.searchParams.get("state");
149
135
  const errorParam = url.searchParams.get("error");
150
136
  if (errorParam) {
137
+ emit(false, errorParam);
151
138
  return { ok: false, returnTo: "/", error: errorParam };
152
139
  }
153
140
  if (!code || !state) {
141
+ emit(false, "missing_code_or_state");
154
142
  return { ok: false, returnTo: "/", error: "missing_code_or_state" };
155
143
  }
156
144
  const record = loadPkce(state);
157
145
  if (!record) {
146
+ emit(false, "unknown_state");
158
147
  return { ok: false, returnTo: "/", error: "unknown_state" };
159
148
  }
160
149
  clearPkce(state);
161
150
  const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
162
151
  if (!fetchImpl) {
152
+ emit(false, "no_fetch");
163
153
  return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
164
154
  }
165
155
  const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
@@ -178,10 +168,12 @@ async function handleAuthCallback(manager, options = {}) {
178
168
  const body = await res.json().catch(() => ({}));
179
169
  if (!res.ok) {
180
170
  const desc = body.error_description ?? body.error ?? "token_exchange_failed";
171
+ emit(false, desc);
181
172
  return { ok: false, returnTo: record.returnTo, error: desc };
182
173
  }
183
174
  const tokens = body;
184
175
  if (!tokens.access_token) {
176
+ emit(false, "missing_access_token");
185
177
  return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
186
178
  }
187
179
  if (tokens.refresh_token) {
@@ -189,21 +181,24 @@ async function handleAuthCallback(manager, options = {}) {
189
181
  setCookie(cookieName, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
190
182
  }
191
183
  manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
184
+ emit(true);
192
185
  return { ok: true, returnTo: record.returnTo };
193
186
  }
194
187
  async function signOut(manager, opts = {}) {
195
188
  if (!opts.localOnly) {
196
189
  const issuer = manager.issuerUrl.replace(/\/$/, "");
190
+ const idempotency = manager.getIdempotencyToken();
197
191
  try {
198
192
  const url = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
199
- await manager.fetch(url, { method: "POST" }).catch(() => void 0);
193
+ await manager.fetch(url, { method: "POST", headers: { "X-IQAuth-Idempotency": idempotency } }).catch(() => void 0);
200
194
  } catch {
201
195
  }
202
196
  if (opts.endSsoSession !== false) {
203
197
  try {
204
198
  await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
205
199
  method: "POST",
206
- credentials: "include"
200
+ credentials: "include",
201
+ headers: { "X-IQAuth-Idempotency": idempotency }
207
202
  }).catch(() => void 0);
208
203
  } catch {
209
204
  }
@@ -221,9 +216,6 @@ export {
221
216
  setCookie,
222
217
  getCookie,
223
218
  clearCookie,
224
- randomUrlSafe,
225
- s256Challenge,
226
- createPkcePair,
227
219
  buildSignInUrl,
228
220
  redirectToSignIn,
229
221
  signIn,
@@ -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);