@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.
- package/README.md +173 -1
- package/dist/browser-session.d.mts +4 -4
- package/dist/browser-session.d.ts +4 -4
- package/dist/browser-session.js +181 -41
- package/dist/browser-session.mjs +3 -3
- package/dist/browser.d.mts +5 -5
- package/dist/browser.d.ts +5 -5
- package/dist/browser.js +271 -32
- package/dist/browser.mjs +10 -8
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- package/dist/chunk-C2ZTBOAC.mjs +36 -0
- package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
- package/dist/chunk-GLXSIGVS.mjs +66 -0
- package/dist/{chunk-TKZTCPEK.mjs → chunk-GN37E64I.mjs} +32 -40
- package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
- package/dist/{chunk-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
- package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
- package/dist/chunk-PMAFENVI.mjs +229 -0
- package/dist/chunk-RR2MGPTK.mjs +2724 -0
- package/dist/{chunk-76W5TLQQ.mjs → chunk-RTJAIBXY.mjs} +220 -20
- package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
- package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
- package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
- package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
- package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
- package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
- package/dist/errors-Jl1Jtm-6.d.mts +107 -0
- package/dist/errors-Jl1Jtm-6.d.ts +107 -0
- package/dist/{express-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
- package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
- package/dist/express.d.mts +7 -6
- package/dist/express.d.ts +7 -6
- package/dist/express.js +349 -52
- package/dist/express.mjs +39 -12
- package/dist/fastify.d.mts +2 -0
- package/dist/fastify.d.ts +2 -0
- package/dist/fastify.js +332 -52
- package/dist/fastify.mjs +23 -8
- package/dist/hono.d.mts +2 -0
- package/dist/hono.d.ts +2 -0
- package/dist/hono.js +329 -52
- package/dist/hono.mjs +20 -8
- package/dist/index-5KSZEnDe.d.ts +1626 -0
- package/dist/index-CKoZHAoc.d.mts +1626 -0
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +565 -69
- package/dist/index.mjs +29 -9
- package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
- package/dist/locales.d.mts +1 -1
- package/dist/locales.d.ts +1 -1
- package/dist/mobile.d.mts +77 -7
- package/dist/mobile.d.ts +77 -7
- package/dist/mobile.js +276 -41
- package/dist/mobile.mjs +98 -3
- package/dist/next.d.mts +2 -1
- package/dist/next.d.ts +2 -1
- package/dist/next.js +391 -201
- package/dist/next.mjs +22 -7
- package/dist/pkce-7WKV4OIN.mjs +11 -0
- package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
- package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
- package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
- package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
- package/dist/react-permissions.d.mts +52 -0
- package/dist/react-permissions.d.ts +52 -0
- package/dist/react-permissions.js +239 -0
- package/dist/react-permissions.mjs +97 -0
- package/dist/react.d.mts +9 -1624
- package/dist/react.d.ts +9 -1624
- package/dist/react.js +343 -36
- package/dist/react.mjs +59 -2611
- package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
- package/dist/server/handlers.d.mts +148 -3
- package/dist/server/handlers.d.ts +148 -3
- package/dist/server/handlers.js +410 -11
- package/dist/server/handlers.mjs +12 -3
- package/dist/server.d.mts +151 -8
- package/dist/server.d.ts +151 -8
- package/dist/server.js +406 -50
- package/dist/server.mjs +93 -11
- package/dist/service.d.mts +4 -4
- package/dist/service.d.ts +4 -4
- package/dist/service.js +181 -41
- package/dist/service.mjs +3 -3
- package/dist/{signIn-CiIBTJIh.d.mts → signIn-BLFnz8SV.d.ts} +78 -3
- package/dist/{signIn-CCY4JE5G.mjs → signIn-SHBW6Z4T.mjs} +2 -1
- package/dist/{signIn-OCr88Zf8.d.ts → signIn-T-CZ6t6r.d.mts} +78 -3
- package/dist/test.mjs +3 -3
- package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
- package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
- package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
- package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
- package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
- package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
- package/dist/webhooks.d.mts +100 -17
- package/dist/webhooks.d.ts +100 -17
- package/dist/webhooks.js +164 -15
- package/dist/webhooks.mjs +7 -1
- package/dist/ws.d.mts +2 -2
- package/dist/ws.d.ts +2 -2
- package/dist/ws.js +80 -30
- package/dist/ws.mjs +4 -4
- package/docs/error-handling.md +101 -0
- package/docs/guides/effective-permissions.md +171 -0
- package/package.json +13 -3
- package/dist/chunk-UKZLOHZG.mjs +0 -83
- package/dist/errors-CDdl24MP.d.mts +0 -52
- 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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 (
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
94
|
-
const matched = v1.some((sig) =>
|
|
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-
|
|
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-
|
|
2
|
-
import { J as JwtClaims } from './types-
|
|
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-
|
|
2
|
-
import { J as JwtClaims } from './types-
|
|
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,
|
|
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.
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
120
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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
|
-
"
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
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.
|