@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
package/dist/webhooks.js CHANGED
@@ -30,8 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/webhooks.ts
31
31
  var webhooks_exports = {};
32
32
  __export(webhooks_exports, {
33
+ IQAUTH_SIGNATURE_HEADER: () => IQAUTH_SIGNATURE_HEADER,
34
+ LEGACY_SIGNATURE_HEADERS: () => LEGACY_SIGNATURE_HEADERS,
33
35
  WebhookSignatureError: () => WebhookSignatureError,
34
36
  isValidWebhookSignature: () => isValidWebhookSignature,
37
+ parseWebhookEvent: () => parseWebhookEvent,
35
38
  verifyWebhookSignature: () => verifyWebhookSignature
36
39
  });
37
40
  module.exports = __toCommonJS(webhooks_exports);
@@ -43,6 +46,12 @@ var WebhookSignatureError = class extends Error {
43
46
  this.code = code;
44
47
  }
45
48
  };
49
+ var IQAUTH_SIGNATURE_HEADER = "x-iqauth-signature";
50
+ var LEGACY_SIGNATURE_HEADERS = [
51
+ "x-webhook-signature",
52
+ "x-iq-auth-signature",
53
+ "x-signature"
54
+ ];
46
55
  function toBuffer(p) {
47
56
  if (typeof p === "string") return Buffer.from(p, "utf8");
48
57
  if (Buffer.isBuffer(p)) return p;
@@ -51,13 +60,19 @@ function toBuffer(p) {
51
60
  function parseHeader(header) {
52
61
  let t = NaN;
53
62
  const v1 = [];
54
- for (const part of header.split(",")) {
55
- const [k, v] = part.split("=", 2);
56
- if (!k || v === void 0) continue;
57
- const key = k.trim();
58
- const value = v.trim();
63
+ const trimmed = header.trim();
64
+ if (/^[0-9a-f]+$/i.test(trimmed)) {
65
+ v1.push(trimmed.toLowerCase());
66
+ return { t, v1 };
67
+ }
68
+ for (const part of trimmed.split(",")) {
69
+ const eqIdx = part.indexOf("=");
70
+ if (eqIdx === -1) continue;
71
+ const key = part.slice(0, eqIdx).trim().toLowerCase();
72
+ const value = part.slice(eqIdx + 1).trim();
73
+ if (!value) continue;
59
74
  if (key === "t") t = Number(value);
60
- else if (key === "v1") v1.push(value);
75
+ else if (key === "v1") v1.push(value.toLowerCase());
61
76
  }
62
77
  return { t, v1 };
63
78
  }
@@ -69,6 +84,11 @@ function timingSafeEqualHex(a, b) {
69
84
  return false;
70
85
  }
71
86
  }
87
+ function computeSignatures(secret, body, t) {
88
+ const modern = import_crypto.default.createHmac("sha256", secret).update(body).digest("hex");
89
+ const legacy = Number.isFinite(t) ? import_crypto.default.createHmac("sha256", secret).update(`${t}.`).update(body).digest("hex") : null;
90
+ return { modern, legacy };
91
+ }
72
92
  function verifyWebhookSignature(opts) {
73
93
  const headerRaw = Array.isArray(opts.header) ? opts.header[0] : opts.header;
74
94
  if (!headerRaw || typeof headerRaw !== "string") {
@@ -78,20 +98,27 @@ function verifyWebhookSignature(opts) {
78
98
  throw new WebhookSignatureError("MISSING_SECRET", "secret is required");
79
99
  }
80
100
  const { t, v1 } = parseHeader(headerRaw);
81
- if (!Number.isFinite(t) || v1.length === 0) {
101
+ if (v1.length === 0) {
82
102
  throw new WebhookSignatureError("MALFORMED_HEADER", `Could not parse signature header: ${headerRaw}`);
83
103
  }
84
104
  const tolerance = opts.toleranceSeconds ?? 300;
85
- const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
86
- if (Math.abs(now - t) > tolerance) {
87
- throw new WebhookSignatureError(
88
- "TIMESTAMP_OUT_OF_TOLERANCE",
89
- `Signature timestamp ${t} is outside the ${tolerance}s tolerance window (now=${now})`
90
- );
105
+ if (Number.isFinite(t)) {
106
+ const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
107
+ if (Math.abs(now - t) > tolerance) {
108
+ throw new WebhookSignatureError(
109
+ "TIMESTAMP_OUT_OF_TOLERANCE",
110
+ `Signature timestamp ${t} is outside the ${tolerance}s tolerance window (now=${now})`
111
+ );
112
+ }
91
113
  }
92
114
  const body = toBuffer(opts.payload);
93
- const expected = import_crypto.default.createHmac("sha256", opts.secret).update(`${t}.`).update(body).digest("hex");
94
- const matched = v1.some((sig) => timingSafeEqualHex(sig, expected));
115
+ const { modern, legacy } = computeSignatures(opts.secret, body, t);
116
+ const matched = v1.some((sig) => {
117
+ const lower = sig.toLowerCase();
118
+ if (timingSafeEqualHex(lower, modern)) return true;
119
+ if (legacy && timingSafeEqualHex(lower, legacy)) return true;
120
+ return false;
121
+ });
95
122
  if (!matched) {
96
123
  throw new WebhookSignatureError("SIGNATURE_MISMATCH", "Webhook signature does not match expected value");
97
124
  }
@@ -111,9 +138,131 @@ function isValidWebhookSignature(opts) {
111
138
  return false;
112
139
  }
113
140
  }
141
+ function readHeader(headers, name) {
142
+ if (typeof headers.get === "function") {
143
+ return headers.get(name);
144
+ }
145
+ const lower = name.toLowerCase();
146
+ const obj = headers;
147
+ if (lower in obj) return obj[lower];
148
+ for (const [k, v] of Object.entries(obj)) {
149
+ if (k.toLowerCase() === lower) return v;
150
+ }
151
+ return void 0;
152
+ }
153
+ function pickHeaderValue(value) {
154
+ if (value == null) return null;
155
+ if (Array.isArray(value)) return value[0] ?? null;
156
+ return value;
157
+ }
158
+ function envelopeError(message) {
159
+ throw new WebhookSignatureError("MALFORMED_ENVELOPE", message);
160
+ }
161
+ function parseWebhookEvent(rawBody, headers, secrets, opts = {}) {
162
+ if (!Array.isArray(secrets) || secrets.length === 0 || secrets.every((s) => !s)) {
163
+ throw new WebhookSignatureError("MISSING_SECRET", "At least one signing secret is required");
164
+ }
165
+ let headerValue = pickHeaderValue(readHeader(headers, IQAUTH_SIGNATURE_HEADER));
166
+ let usedHeader = IQAUTH_SIGNATURE_HEADER;
167
+ if (!headerValue) {
168
+ for (const legacy of LEGACY_SIGNATURE_HEADERS) {
169
+ const v = pickHeaderValue(readHeader(headers, legacy));
170
+ if (v) {
171
+ headerValue = v;
172
+ usedHeader = legacy;
173
+ const log = opts.onDeprecation ?? ((m) => console.warn(m));
174
+ log(
175
+ `[iqauth] deprecation: webhook delivery used legacy header "${legacy}"; migrate sender to "X-IQAuth-Signature" (back-compat removed in next minor).`
176
+ );
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ if (!headerValue) {
182
+ throw new WebhookSignatureError(
183
+ "MISSING_HEADER",
184
+ `Missing webhook signature header. Expected "X-IQAuth-Signature" (or one of: ${LEGACY_SIGNATURE_HEADERS.join(", ")}).`
185
+ );
186
+ }
187
+ const { t, v1 } = parseHeader(headerValue);
188
+ if (v1.length === 0) {
189
+ throw new WebhookSignatureError(
190
+ "MALFORMED_HEADER",
191
+ `Could not parse "${usedHeader}" header value: ${headerValue}`
192
+ );
193
+ }
194
+ const body = toBuffer(rawBody);
195
+ let verifiedIdx = -1;
196
+ for (let i = 0; i < secrets.length; i++) {
197
+ const secret = secrets[i];
198
+ if (!secret) continue;
199
+ const { modern, legacy } = computeSignatures(secret, body, t);
200
+ const ok = v1.some((sig) => {
201
+ const lower = sig.toLowerCase();
202
+ if (timingSafeEqualHex(lower, modern)) return true;
203
+ if (legacy && timingSafeEqualHex(lower, legacy)) return true;
204
+ return false;
205
+ });
206
+ if (ok) {
207
+ verifiedIdx = i;
208
+ break;
209
+ }
210
+ }
211
+ if (verifiedIdx === -1) {
212
+ throw new WebhookSignatureError(
213
+ "SIGNATURE_MISMATCH",
214
+ "Webhook signature does not match any provided secret"
215
+ );
216
+ }
217
+ let parsed;
218
+ try {
219
+ parsed = JSON.parse(body.toString("utf8"));
220
+ } catch {
221
+ throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
222
+ }
223
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
224
+ envelopeError("Webhook body must be a JSON object");
225
+ }
226
+ const { id, type, subject, time, data, tenantId, specversion } = parsed;
227
+ if (specversion !== "1.0") {
228
+ envelopeError(`Envelope \`specversion\` must be "1.0" (got: ${JSON.stringify(specversion)})`);
229
+ }
230
+ if (typeof id !== "string" || !id) envelopeError("Envelope missing required string `id`");
231
+ if (typeof type !== "string" || !type) envelopeError("Envelope missing required string `type`");
232
+ if (typeof subject !== "string" || !subject) envelopeError("Envelope missing required string `subject`");
233
+ if (typeof time !== "string" || !time) envelopeError("Envelope missing required string `time`");
234
+ if (typeof tenantId !== "string" || !tenantId) envelopeError("Envelope missing required string `tenantId`");
235
+ if (data === void 0 || data === null || typeof data !== "object" || Array.isArray(data)) {
236
+ envelopeError("Envelope `data` must be an object");
237
+ }
238
+ const tolerance = opts.toleranceSeconds ?? 300;
239
+ const eventMs = Date.parse(time);
240
+ if (!Number.isFinite(eventMs)) envelopeError(`Envelope \`time\` is not a valid ISO timestamp: ${time}`);
241
+ const nowMs = opts.nowMs ?? Date.now();
242
+ if (Math.abs(nowMs - eventMs) > tolerance * 1e3) {
243
+ throw new WebhookSignatureError(
244
+ "TIMESTAMP_OUT_OF_TOLERANCE",
245
+ `Envelope time ${time} is outside the ${tolerance}s tolerance window (now=${new Date(nowMs).toISOString()})`
246
+ );
247
+ }
248
+ return {
249
+ specversion: "1.0",
250
+ id,
251
+ type,
252
+ subject,
253
+ time,
254
+ tenantId,
255
+ data,
256
+ idempotencyKey: id,
257
+ verifiedWithSecretIndex: verifiedIdx
258
+ };
259
+ }
114
260
  // Annotate the CommonJS export names for ESM import in node:
115
261
  0 && (module.exports = {
262
+ IQAUTH_SIGNATURE_HEADER,
263
+ LEGACY_SIGNATURE_HEADERS,
116
264
  WebhookSignatureError,
117
265
  isValidWebhookSignature,
266
+ parseWebhookEvent,
118
267
  verifyWebhookSignature
119
268
  });
package/dist/webhooks.mjs CHANGED
@@ -1,11 +1,17 @@
1
1
  import {
2
+ IQAUTH_SIGNATURE_HEADER,
3
+ LEGACY_SIGNATURE_HEADERS,
2
4
  WebhookSignatureError,
3
5
  isValidWebhookSignature,
6
+ parseWebhookEvent,
4
7
  verifyWebhookSignature
5
- } from "./chunk-UKZLOHZG.mjs";
8
+ } from "./chunk-PMAFENVI.mjs";
6
9
  import "./chunk-Y6FXYEAI.mjs";
7
10
  export {
11
+ IQAUTH_SIGNATURE_HEADER,
12
+ LEGACY_SIGNATURE_HEADERS,
8
13
  WebhookSignatureError,
9
14
  isValidWebhookSignature,
15
+ parseWebhookEvent,
10
16
  verifyWebhookSignature
11
17
  };
package/dist/ws.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as TokenVerifyOptions } from './tokens-DCyzzn8L.mjs';
2
- import { J as JwtClaims } from './types-DZAflmmq.mjs';
1
+ import { c as TokenVerifyOptions } from './tokens-CITeoG6P.mjs';
2
+ import { J as JwtClaims } from './types-XOV9XPVi.mjs';
3
3
 
4
4
  /**
5
5
  * @iqauth/sdk/ws — WebSocket upgrade auth helper.
package/dist/ws.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as TokenVerifyOptions } from './tokens-aHiGFr_E.js';
2
- import { J as JwtClaims } from './types-DZAflmmq.js';
1
+ import { c as TokenVerifyOptions } from './tokens-Bqhmqq_R.js';
2
+ import { J as JwtClaims } from './types-XOV9XPVi.js';
3
3
 
4
4
  /**
5
5
  * @iqauth/sdk/ws — WebSocket upgrade auth helper.
package/dist/ws.js CHANGED
@@ -29,13 +29,30 @@ module.exports = __toCommonJS(ws_exports);
29
29
  var import_jose = require("jose");
30
30
 
31
31
  // src/errors.ts
32
- var IQAuthError = class extends Error {
33
- constructor(code, message, status, raw) {
32
+ var IQAuthError = class _IQAuthError extends Error {
33
+ constructor(code, message, status, cause) {
34
34
  super(message);
35
35
  this.name = "IQAuthError";
36
36
  this.code = code;
37
37
  this.status = status;
38
- this.raw = raw;
38
+ this.cause = cause;
39
+ this.raw = cause;
40
+ }
41
+ /**
42
+ * Type guard: true when `value` is an `IQAuthError`. Useful for adapters
43
+ * that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
44
+ */
45
+ static isIQAuthError(value) {
46
+ return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
47
+ }
48
+ /**
49
+ * Type-narrowed code check. Lets callers write
50
+ * `if (err.is("token_expired")) …` with full IntelliSense for the typed
51
+ * taxonomy without losing the ability to handle server codes via
52
+ * `err.code === "TOKEN_REVOKED"`.
53
+ */
54
+ is(code) {
55
+ return this.code === code;
39
56
  }
40
57
  };
41
58
 
@@ -52,6 +69,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
52
69
  "iqvalidate"
53
70
  ];
54
71
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
72
+ function classifyJoseError(err) {
73
+ if (err instanceof import_jose.errors.JWTExpired) {
74
+ return { code: "token_expired", message: "Token has expired" };
75
+ }
76
+ if (err instanceof import_jose.errors.JOSEError) {
77
+ return { code: "token_invalid", message: err.message };
78
+ }
79
+ if (err instanceof Error) {
80
+ return { code: "token_invalid", message: err.message };
81
+ }
82
+ return { code: "token_invalid", message: "Token verification failed" };
83
+ }
55
84
  function decodeProtectedHeader(token) {
56
85
  const parts = token.split(".");
57
86
  if (parts.length < 2) return null;
@@ -88,11 +117,11 @@ var TokensModule = class {
88
117
  async verify(token, options = {}) {
89
118
  const header = decodeProtectedHeader(token);
90
119
  if (!header) {
91
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token");
120
+ throw new IQAuthError("token_invalid", "Unable to decode token");
92
121
  }
93
122
  const kid = header.kid;
94
123
  if (!kid) {
95
- throw new IQAuthError("TOKEN_INVALID", "Token missing kid header");
124
+ throw new IQAuthError("token_invalid", "Token missing kid header");
96
125
  }
97
126
  let cache = await this.ensureCache();
98
127
  if (!cache.byKid.has(kid)) {
@@ -100,7 +129,7 @@ var TokensModule = class {
100
129
  cache = await this.ensureCache();
101
130
  }
102
131
  if (!cache.byKid.has(kid)) {
103
- throw new IQAuthError("TOKEN_INVALID", `Unknown key ID: ${kid}`);
132
+ throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
104
133
  }
105
134
  const issuer = options.issuer ?? this.defaultIssuer;
106
135
  const audience = options.audience ?? this.defaultAudience;
@@ -116,16 +145,8 @@ var TokensModule = class {
116
145
  const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
117
146
  return payload;
118
147
  } catch (err) {
119
- if (err instanceof import_jose.errors.JWTExpired) {
120
- throw new IQAuthError("TOKEN_EXPIRED", "Token has expired");
121
- }
122
- if (err instanceof import_jose.errors.JOSEError) {
123
- throw new IQAuthError("TOKEN_INVALID", err.message);
124
- }
125
- if (err instanceof Error) {
126
- throw new IQAuthError("TOKEN_INVALID", err.message);
127
- }
128
- throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
148
+ const classified = classifyJoseError(err);
149
+ throw new IQAuthError(classified.code, classified.message, void 0, err);
129
150
  }
130
151
  }
131
152
  /**
@@ -167,7 +188,7 @@ var TokensModule = class {
167
188
  getClaims(token) {
168
189
  const claims = this.decode(token);
169
190
  if (!claims) {
170
- throw new IQAuthError("TOKEN_INVALID", "Unable to decode token claims");
191
+ throw new IQAuthError("token_invalid", "Unable to decode token claims");
171
192
  }
172
193
  return claims;
173
194
  }
@@ -177,7 +198,7 @@ var TokensModule = class {
177
198
  }
178
199
  await this.refreshJwks();
179
200
  if (!this.jwksCache) {
180
- throw new IQAuthError("INTERNAL_ERROR", "JWKS cache unavailable after refresh");
201
+ throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
181
202
  }
182
203
  return this.jwksCache;
183
204
  }
@@ -187,22 +208,38 @@ var TokensModule = class {
187
208
  }
188
209
  this.inFlightRefresh = (async () => {
189
210
  try {
190
- const res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
211
+ let res;
212
+ try {
213
+ res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
214
+ } catch (err) {
215
+ throw new IQAuthError(
216
+ "network",
217
+ err instanceof Error ? err.message : "JWKS fetch network error",
218
+ void 0,
219
+ err
220
+ );
221
+ }
191
222
  if (!res.ok) {
192
223
  throw new IQAuthError(
193
- "INTERNAL_ERROR",
194
- `Failed to fetch JWKS: ${res.status}`
224
+ "jwks_fetch_failed",
225
+ `Failed to fetch JWKS: ${res.status}`,
226
+ res.status
195
227
  );
196
228
  }
197
229
  let jwks;
198
230
  try {
199
231
  jwks = await res.json();
200
- } catch {
201
- throw new IQAuthError("INTERNAL_ERROR", "Malformed JWKS response: invalid JSON");
232
+ } catch (err) {
233
+ throw new IQAuthError(
234
+ "jwks_fetch_failed",
235
+ "Malformed JWKS response: invalid JSON",
236
+ res.status,
237
+ err
238
+ );
202
239
  }
203
240
  if (!jwks || !Array.isArray(jwks.keys)) {
204
241
  throw new IQAuthError(
205
- "INTERNAL_ERROR",
242
+ "jwks_fetch_failed",
206
243
  "Malformed JWKS response: expected { keys: [...] }"
207
244
  );
208
245
  }
@@ -210,7 +247,7 @@ var TokensModule = class {
210
247
  for (const key of jwks.keys) {
211
248
  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")) {
212
249
  throw new IQAuthError(
213
- "INTERNAL_ERROR",
250
+ "jwks_fetch_failed",
214
251
  "Malformed JWKS response: key missing required fields"
215
252
  );
216
253
  }
@@ -228,6 +265,19 @@ var TokensModule = class {
228
265
  clearCache() {
229
266
  this.jwksCache = null;
230
267
  }
268
+ /**
269
+ * Task #126: Eagerly populate the JWKS cache so the first verify() call
270
+ * doesn't pay a network round-trip. Safe to call repeatedly — single-flight
271
+ * behavior is shared with the lazy refresh path. Errors are swallowed so
272
+ * callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
273
+ */
274
+ async prewarm() {
275
+ if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
276
+ try {
277
+ await this.refreshJwks();
278
+ } catch {
279
+ }
280
+ }
231
281
  };
232
282
 
233
283
  // src/publishableKey.ts
@@ -259,14 +309,14 @@ function assertPublishableKey(raw, opts) {
259
309
  const ctx = opts?.context ? `${opts.context}: ` : "";
260
310
  if (typeof raw !== "string" || raw.length === 0) {
261
311
  throw new IQAuthError(
262
- "CONFIG_INVALID",
312
+ "config_invalid",
263
313
  `${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.`
264
314
  );
265
315
  }
266
316
  const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
267
317
  if (!shapeMatch) {
268
318
  throw new IQAuthError(
269
- "CONFIG_INVALID",
319
+ "config_invalid",
270
320
  `${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.`
271
321
  );
272
322
  }
@@ -275,19 +325,19 @@ function assertPublishableKey(raw, opts) {
275
325
  decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
276
326
  } catch {
277
327
  throw new IQAuthError(
278
- "CONFIG_INVALID",
328
+ "config_invalid",
279
329
  `${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
280
330
  );
281
331
  }
282
332
  if (!isPublishableKeyPayload(decoded)) {
283
333
  throw new IQAuthError(
284
- "CONFIG_INVALID",
334
+ "config_invalid",
285
335
  `${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
286
336
  );
287
337
  }
288
338
  if (!isValidIssuerUrl(decoded.iss)) {
289
339
  throw new IQAuthError(
290
- "CONFIG_INVALID",
340
+ "config_invalid",
291
341
  `${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.`
292
342
  );
293
343
  }
package/dist/ws.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  _resetWsVerifierCache,
3
3
  verifyWsUpgrade
4
- } from "./chunk-3JULWS6F.mjs";
5
- import "./chunk-WQWBJSSS.mjs";
6
- import "./chunk-UNYDG2L4.mjs";
7
- import "./chunk-6I6RM4MN.mjs";
4
+ } from "./chunk-WCELYTJ3.mjs";
5
+ import "./chunk-HVHNYPDC.mjs";
6
+ import "./chunk-NUO2I65G.mjs";
7
+ import "./chunk-6PJRLRB4.mjs";
8
8
  import "./chunk-Y6FXYEAI.mjs";
9
9
  export {
10
10
  _resetWsVerifierCache,
@@ -0,0 +1,101 @@
1
+ # Error handling (`@iqauth/sdk` ≥ 2.7.0)
2
+
3
+ The SDK throws a single error class — `IQAuthError` — with a normalized
4
+ `code` field that you can pattern-match exhaustively.
5
+
6
+ ## The taxonomy
7
+
8
+ ```ts
9
+ import { IQAuthError, type IQAuthErrorCode } from "@iqauth/sdk";
10
+
11
+ type IQAuthErrorCode =
12
+ | "token_expired" // jwtVerify said the token is past `exp`
13
+ | "token_invalid" // bad signature, wrong issuer/audience, malformed JWT
14
+ | "jwks_unavailable" // local JWKS cache could not be populated
15
+ | "jwks_fetch_failed" // issuer responded but JWKS payload was bad / non-2xx
16
+ | "rate_limited" // upstream returned 429 (server-originated)
17
+ | "network" // DNS, TCP, TLS, abort — issuer is unreachable
18
+ | "config_invalid" // publishable key / SDK config is malformed
19
+ | "app_not_found" // app id/key in the publishable key isn't recognized
20
+ | "permission_denied" // claims valid but caller lacks the required scope
21
+ | "unknown"; // catch-all
22
+ ```
23
+
24
+ Use `IQAuthError.isIQAuthError(value)` for the type guard, and `err.is(code)`
25
+ for narrowed code checks:
26
+
27
+ ```ts
28
+ import { IQAuthError } from "@iqauth/sdk";
29
+
30
+ try {
31
+ const claims = await client.tokens.verify(accessToken);
32
+ } catch (err) {
33
+ if (!IQAuthError.isIQAuthError(err)) throw err;
34
+
35
+ if (err.is("token_expired")) return refreshAndRetry();
36
+ if (err.is("token_invalid")) return signOut();
37
+ if (err.is("jwks_fetch_failed")) return retryAfterBackoff();
38
+ if (err.is("network")) return showOfflineBanner();
39
+ // …
40
+ }
41
+ ```
42
+
43
+ ## Backwards compatibility
44
+
45
+ `IQAuthError.code` is typed as `IQAuthErrorCode | (string & {})` — the
46
+ `(string & {})` widens the union to "any string" while preserving
47
+ auto-complete for the typed values. This matters because:
48
+
49
+ - **Server-originated errors** (rethrown from `/oidc/token`, `/api/v1/...`,
50
+ the SAML endpoints, etc.) keep their UPPER_SNAKE codes (`TOKEN_REVOKED`,
51
+ `MFA_INVALID_CODE`, `SESSION_EXPIRED_INACTIVITY`, …). The SDK does not
52
+ rewrite them — they are passed through verbatim.
53
+ - **Framework adapters** (`@iqauth/sdk/express`, `/fastify`, `/hono`) map
54
+ *both* the legacy UPPER_SNAKE codes and the new lowercase typed codes to
55
+ 401, so apps that have a `case "TOKEN_EXPIRED"` switch keep working.
56
+ - **`err.cause`** preserves the originating error (e.g. the underlying
57
+ `JOSEError` from `jose`, or the `TypeError` from `fetch`). The legacy
58
+ `err.raw` alias is still populated.
59
+
60
+ If you only catch `Error`, nothing in your code needs to change. If you
61
+ were string-matching on `err.message`, switch to `err.is("…")`.
62
+
63
+ ## Where each code is thrown
64
+
65
+ | Code | Throw site |
66
+ | --- | --- |
67
+ | `token_expired` | `tokens.verify` — jose raises `JWTExpired` |
68
+ | `token_invalid` | `tokens.verify` — bad sig / wrong iss / wrong aud / unknown kid / malformed header / `tokens.getClaims` on undecodable input; `oidc.handleCallback` — id_token nonce mismatch; `http.ts` refresh — server returned a malformed token pair |
69
+ | `jwks_unavailable` | `tokens.verify` — JWKS cache still empty after refresh |
70
+ | `jwks_fetch_failed` | `tokens.verify` — JWKS endpoint returned non-2xx, invalid JSON, or a malformed JWKS shape |
71
+ | `network` | `tokens.verify` — `fetch()` itself rejected (DNS, TLS, abort) |
72
+ | `rate_limited` | Reserved for future SDK-side throttling; today this code only appears on server-rethrown 429s |
73
+ | `config_invalid` | `assertPublishableKey()` — bad shape, bad version, missing fields; `oidc.handleCallback` — missing/unknown `state` or `code`, or no `TokensModule` wired up; `http.ts` refresh — caller didn't provide a refresh token |
74
+ | `app_not_found` | Reserved for future use — today the server returns `APP_NOT_FOUND` and the SDK passes it through |
75
+ | `permission_denied` | Reserved for SDK-side scope checks — today the server returns `INSUFFICIENT_PERMISSIONS` |
76
+ | `unknown` | Catch-all |
77
+
78
+ ## Typed claims via `verify<T>()`
79
+
80
+ `tokens.verify<T>()` is generic over your app's custom JWT-template claims
81
+ and returns `IQAuthClaims<T> & JwtClaims`:
82
+
83
+ ```ts
84
+ import type { IQAuthClaims } from "@iqauth/sdk";
85
+
86
+ interface MyClaims {
87
+ plan: "free" | "pro";
88
+ orgId: string;
89
+ }
90
+
91
+ const claims = await client.tokens.verify<MyClaims>(accessToken);
92
+ // ^? IQAuthClaims<MyClaims> & JwtClaims
93
+
94
+ if (claims.plan === "pro") doProThing(claims.orgId);
95
+ console.log(claims.tenantId, claims.sub); // standard fields still typed
96
+ ```
97
+
98
+ `IQAuthClaims<T>` documents the OIDC standard fields the issuer guarantees
99
+ (`sub`, `iss`, `aud`, `exp`, `iat`) plus IQAuth tenant/vendor/role/session
100
+ claims as optional. The `[key: string]: unknown` index signature lets
101
+ custom claims through at runtime even when no generic is supplied.