@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.
- 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 +5 -5
- package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
- package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
- package/dist/chunk-GLXSIGVS.mjs +66 -0
- package/dist/{chunk-DJIBN2N7.mjs → chunk-GN37E64I.mjs} +29 -7
- 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-XAWYUPMO.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/{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 +313 -33
- package/dist/react.mjs +58 -2632
- 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-OCr88Zf8.d.ts → signIn-BLFnz8SV.d.ts} +78 -3
- package/dist/{signIn-4OKLDEIH.mjs → signIn-SHBW6Z4T.mjs} +1 -1
- package/dist/{signIn-CiIBTJIh.d.mts → 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/server/handlers.js
CHANGED
|
@@ -20,21 +20,42 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/server/handlers.ts
|
|
21
21
|
var handlers_exports = {};
|
|
22
22
|
__export(handlers_exports, {
|
|
23
|
+
__resetSignoutMarkersForTests: () => __resetSignoutMarkersForTests,
|
|
24
|
+
buildUserinfoResponse: () => buildUserinfoResponse,
|
|
25
|
+
createInMemorySignoutRegistry: () => createInMemorySignoutRegistry,
|
|
23
26
|
handleCallback: () => handleCallback,
|
|
24
27
|
handleRefresh: () => handleRefresh,
|
|
25
28
|
handleSignout: () => handleSignout,
|
|
29
|
+
handleUserinfo: () => handleUserinfo,
|
|
26
30
|
serializeCookie: () => serializeCookie
|
|
27
31
|
});
|
|
28
32
|
module.exports = __toCommonJS(handlers_exports);
|
|
29
33
|
|
|
30
34
|
// src/errors.ts
|
|
31
|
-
var IQAuthError = class extends Error {
|
|
32
|
-
constructor(code, message, status,
|
|
35
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
36
|
+
constructor(code, message, status, cause) {
|
|
33
37
|
super(message);
|
|
34
38
|
this.name = "IQAuthError";
|
|
35
39
|
this.code = code;
|
|
36
40
|
this.status = status;
|
|
37
|
-
this.
|
|
41
|
+
this.cause = cause;
|
|
42
|
+
this.raw = cause;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
46
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
47
|
+
*/
|
|
48
|
+
static isIQAuthError(value) {
|
|
49
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Type-narrowed code check. Lets callers write
|
|
53
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
54
|
+
* taxonomy without losing the ability to handle server codes via
|
|
55
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
56
|
+
*/
|
|
57
|
+
is(code) {
|
|
58
|
+
return this.code === code;
|
|
38
59
|
}
|
|
39
60
|
};
|
|
40
61
|
|
|
@@ -67,14 +88,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
67
88
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
68
89
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
69
90
|
throw new IQAuthError(
|
|
70
|
-
"
|
|
91
|
+
"config_invalid",
|
|
71
92
|
`${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.`
|
|
72
93
|
);
|
|
73
94
|
}
|
|
74
95
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
75
96
|
if (!shapeMatch) {
|
|
76
97
|
throw new IQAuthError(
|
|
77
|
-
"
|
|
98
|
+
"config_invalid",
|
|
78
99
|
`${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.`
|
|
79
100
|
);
|
|
80
101
|
}
|
|
@@ -83,19 +104,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
83
104
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
84
105
|
} catch {
|
|
85
106
|
throw new IQAuthError(
|
|
86
|
-
"
|
|
107
|
+
"config_invalid",
|
|
87
108
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
88
109
|
);
|
|
89
110
|
}
|
|
90
111
|
if (!isPublishableKeyPayload(decoded)) {
|
|
91
112
|
throw new IQAuthError(
|
|
92
|
-
"
|
|
113
|
+
"config_invalid",
|
|
93
114
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
94
115
|
);
|
|
95
116
|
}
|
|
96
117
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
97
118
|
throw new IQAuthError(
|
|
98
|
-
"
|
|
119
|
+
"config_invalid",
|
|
99
120
|
`${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.`
|
|
100
121
|
);
|
|
101
122
|
}
|
|
@@ -107,7 +128,267 @@ function isPublishableKeyPayload(value) {
|
|
|
107
128
|
return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
|
|
108
129
|
}
|
|
109
130
|
|
|
131
|
+
// src/modules/tokens.ts
|
|
132
|
+
var import_jose = require("jose");
|
|
133
|
+
var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
134
|
+
var DEFAULT_TOKEN_ISSUER = [
|
|
135
|
+
"https://auth.dispositioniq.com",
|
|
136
|
+
"auth.dispositioniq.com"
|
|
137
|
+
];
|
|
138
|
+
var DEFAULT_TOKEN_AUDIENCE = [
|
|
139
|
+
"dispositioniq",
|
|
140
|
+
"iqcapture",
|
|
141
|
+
"iqreuse",
|
|
142
|
+
"iqvalidate"
|
|
143
|
+
];
|
|
144
|
+
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
145
|
+
function classifyJoseError(err) {
|
|
146
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
147
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
148
|
+
}
|
|
149
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
150
|
+
return { code: "token_invalid", message: err.message };
|
|
151
|
+
}
|
|
152
|
+
if (err instanceof Error) {
|
|
153
|
+
return { code: "token_invalid", message: err.message };
|
|
154
|
+
}
|
|
155
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
156
|
+
}
|
|
157
|
+
function decodeProtectedHeader(token) {
|
|
158
|
+
const parts = token.split(".");
|
|
159
|
+
if (parts.length < 2) return null;
|
|
160
|
+
try {
|
|
161
|
+
const padded = parts[0] + "=".repeat((4 - parts[0].length % 4) % 4);
|
|
162
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
163
|
+
let json;
|
|
164
|
+
if (typeof atob === "function") {
|
|
165
|
+
json = atob(b64);
|
|
166
|
+
} else {
|
|
167
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
168
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
169
|
+
}
|
|
170
|
+
return JSON.parse(json);
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
var TokensModule = class {
|
|
176
|
+
constructor(baseUrl, options = {}) {
|
|
177
|
+
this.jwksCache = null;
|
|
178
|
+
this.inFlightRefresh = null;
|
|
179
|
+
this.baseUrl = baseUrl;
|
|
180
|
+
this.defaultIssuer = options.issuer ?? DEFAULT_TOKEN_ISSUER;
|
|
181
|
+
this.defaultAudience = options.audience ?? DEFAULT_TOKEN_AUDIENCE;
|
|
182
|
+
this.defaultClockTolerance = options.clockTolerance ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Verify a JWT access token using RS256/ES256 via JWKS from
|
|
186
|
+
* `/.well-known/jwks.json`. Backed by `jose` (Web Crypto) so it runs on
|
|
187
|
+
* Node, browser, and edge runtimes alike — no `node:crypto` dependency.
|
|
188
|
+
* Caches JWKS for 1 hour and refetches once on unknown `kid`.
|
|
189
|
+
*/
|
|
190
|
+
async verify(token, options = {}) {
|
|
191
|
+
const header = decodeProtectedHeader(token);
|
|
192
|
+
if (!header) {
|
|
193
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
194
|
+
}
|
|
195
|
+
const kid = header.kid;
|
|
196
|
+
if (!kid) {
|
|
197
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
198
|
+
}
|
|
199
|
+
let cache = await this.ensureCache();
|
|
200
|
+
if (!cache.byKid.has(kid)) {
|
|
201
|
+
this.jwksCache = null;
|
|
202
|
+
cache = await this.ensureCache();
|
|
203
|
+
}
|
|
204
|
+
if (!cache.byKid.has(kid)) {
|
|
205
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
206
|
+
}
|
|
207
|
+
const issuer = options.issuer ?? this.defaultIssuer;
|
|
208
|
+
const audience = options.audience ?? this.defaultAudience;
|
|
209
|
+
const clockTolerance = options.clockTolerance ?? this.defaultClockTolerance;
|
|
210
|
+
const algorithms = options.algorithms ?? ["RS256", "ES256"];
|
|
211
|
+
const verifyOptions = {
|
|
212
|
+
algorithms,
|
|
213
|
+
clockTolerance,
|
|
214
|
+
issuer,
|
|
215
|
+
audience
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
219
|
+
return payload;
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const classified = classifyJoseError(err);
|
|
222
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Decode a JWT without verification. Returns null if malformed.
|
|
227
|
+
*/
|
|
228
|
+
decode(token) {
|
|
229
|
+
try {
|
|
230
|
+
const parts = token.split(".");
|
|
231
|
+
if (parts.length < 2) return null;
|
|
232
|
+
const payload = parts[1];
|
|
233
|
+
const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
234
|
+
const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
235
|
+
let json;
|
|
236
|
+
if (typeof atob === "function") {
|
|
237
|
+
json = atob(b64);
|
|
238
|
+
} else {
|
|
239
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
240
|
+
json = Buffer2.from(b64, "base64").toString("utf8");
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
json = decodeURIComponent(escape(json));
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
const claims = JSON.parse(json);
|
|
247
|
+
if (!claims || typeof claims !== "object") return null;
|
|
248
|
+
return claims;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Check if a token is expired based on the `exp` claim. */
|
|
254
|
+
isExpired(token) {
|
|
255
|
+
const claims = this.decode(token);
|
|
256
|
+
if (!claims?.exp) return true;
|
|
257
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
258
|
+
return claims.exp <= now;
|
|
259
|
+
}
|
|
260
|
+
/** Get the claims from a token without verification. */
|
|
261
|
+
getClaims(token) {
|
|
262
|
+
const claims = this.decode(token);
|
|
263
|
+
if (!claims) {
|
|
264
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
265
|
+
}
|
|
266
|
+
return claims;
|
|
267
|
+
}
|
|
268
|
+
async ensureCache() {
|
|
269
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) {
|
|
270
|
+
return this.jwksCache;
|
|
271
|
+
}
|
|
272
|
+
await this.refreshJwks();
|
|
273
|
+
if (!this.jwksCache) {
|
|
274
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
275
|
+
}
|
|
276
|
+
return this.jwksCache;
|
|
277
|
+
}
|
|
278
|
+
async refreshJwks() {
|
|
279
|
+
if (this.inFlightRefresh) {
|
|
280
|
+
return this.inFlightRefresh;
|
|
281
|
+
}
|
|
282
|
+
this.inFlightRefresh = (async () => {
|
|
283
|
+
try {
|
|
284
|
+
let res;
|
|
285
|
+
try {
|
|
286
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
throw new IQAuthError(
|
|
289
|
+
"network",
|
|
290
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
291
|
+
void 0,
|
|
292
|
+
err
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (!res.ok) {
|
|
296
|
+
throw new IQAuthError(
|
|
297
|
+
"jwks_fetch_failed",
|
|
298
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
299
|
+
res.status
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
let jwks;
|
|
303
|
+
try {
|
|
304
|
+
jwks = await res.json();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
throw new IQAuthError(
|
|
307
|
+
"jwks_fetch_failed",
|
|
308
|
+
"Malformed JWKS response: invalid JSON",
|
|
309
|
+
res.status,
|
|
310
|
+
err
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
314
|
+
throw new IQAuthError(
|
|
315
|
+
"jwks_fetch_failed",
|
|
316
|
+
"Malformed JWKS response: expected { keys: [...] }"
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const byKid = /* @__PURE__ */ new Set();
|
|
320
|
+
for (const key of jwks.keys) {
|
|
321
|
+
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")) {
|
|
322
|
+
throw new IQAuthError(
|
|
323
|
+
"jwks_fetch_failed",
|
|
324
|
+
"Malformed JWKS response: key missing required fields"
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
byKid.add(key.kid);
|
|
328
|
+
}
|
|
329
|
+
const verifier = (0, import_jose.createLocalJWKSet)({ keys: jwks.keys });
|
|
330
|
+
this.jwksCache = { raw: jwks.keys, byKid, verifier, fetchedAt: Date.now() };
|
|
331
|
+
} finally {
|
|
332
|
+
this.inFlightRefresh = null;
|
|
333
|
+
}
|
|
334
|
+
})();
|
|
335
|
+
return this.inFlightRefresh;
|
|
336
|
+
}
|
|
337
|
+
/** @internal Exposed for testing — clears JWKS cache */
|
|
338
|
+
clearCache() {
|
|
339
|
+
this.jwksCache = null;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
343
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
344
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
345
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
346
|
+
*/
|
|
347
|
+
async prewarm() {
|
|
348
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
349
|
+
try {
|
|
350
|
+
await this.refreshJwks();
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
110
356
|
// src/server/handlers.ts
|
|
357
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
358
|
+
const baseUser = {
|
|
359
|
+
sub: claims.sub,
|
|
360
|
+
email: claims.email,
|
|
361
|
+
name: claims.name,
|
|
362
|
+
tenantId: claims.tenantId,
|
|
363
|
+
vendorId: claims.vendorId,
|
|
364
|
+
roles: claims.roles ?? [],
|
|
365
|
+
entitlements: claims.entitlements ?? []
|
|
366
|
+
};
|
|
367
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
368
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
369
|
+
return {
|
|
370
|
+
success: true,
|
|
371
|
+
data: {
|
|
372
|
+
user,
|
|
373
|
+
claims,
|
|
374
|
+
tenantId: claims.tenantId ?? null
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function emitTiming(cfg, event) {
|
|
379
|
+
if (cfg.debug) {
|
|
380
|
+
try {
|
|
381
|
+
console.debug("[iqauth_helper]", event);
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (cfg.onTimingEvent) {
|
|
386
|
+
try {
|
|
387
|
+
cfg.onTimingEvent(event);
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
111
392
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
112
393
|
"TOKEN_REVOKED",
|
|
113
394
|
"SESSION_REVOKED",
|
|
@@ -147,7 +428,11 @@ function resolve(config) {
|
|
|
147
428
|
})),
|
|
148
429
|
appId: parsed.appId,
|
|
149
430
|
tenantId: parsed.tenantId,
|
|
150
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
431
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
432
|
+
debug: config.debug,
|
|
433
|
+
onTimingEvent: config.onTimingEvent,
|
|
434
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
435
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
|
|
151
436
|
};
|
|
152
437
|
}
|
|
153
438
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -164,15 +449,64 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
164
449
|
}
|
|
165
450
|
function clearCookies(cfg) {
|
|
166
451
|
return [
|
|
167
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
168
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
452
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
453
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
169
454
|
];
|
|
170
455
|
}
|
|
456
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
457
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
458
|
+
function pruneInMemoryMarkers(now) {
|
|
459
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
460
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
461
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
var defaultSignoutRegistry = {
|
|
465
|
+
mark(token, ttlMs) {
|
|
466
|
+
const now = Date.now();
|
|
467
|
+
pruneInMemoryMarkers(now);
|
|
468
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
469
|
+
},
|
|
470
|
+
has(token) {
|
|
471
|
+
const now = Date.now();
|
|
472
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
473
|
+
if (!exp) return false;
|
|
474
|
+
if (exp <= now) {
|
|
475
|
+
inMemorySignoutMarkers.delete(token);
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
function __resetSignoutMarkersForTests() {
|
|
482
|
+
inMemorySignoutMarkers.clear();
|
|
483
|
+
}
|
|
484
|
+
function createInMemorySignoutRegistry() {
|
|
485
|
+
const store = /* @__PURE__ */ new Map();
|
|
486
|
+
return {
|
|
487
|
+
mark(token, ttlMs) {
|
|
488
|
+
const now = Date.now();
|
|
489
|
+
for (const [k, exp] of store) if (exp <= now) store.delete(k);
|
|
490
|
+
store.set(token, now + ttlMs);
|
|
491
|
+
},
|
|
492
|
+
has(token) {
|
|
493
|
+
const now = Date.now();
|
|
494
|
+
const exp = store.get(token);
|
|
495
|
+
if (!exp) return false;
|
|
496
|
+
if (exp <= now) {
|
|
497
|
+
store.delete(token);
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
171
504
|
function serializeCookie(d) {
|
|
172
505
|
const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
|
|
173
506
|
parts.push(`Path=${d.path}`);
|
|
174
507
|
if (d.domain) parts.push(`Domain=${d.domain}`);
|
|
175
508
|
parts.push(`Max-Age=${d.maxAge}`);
|
|
509
|
+
if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
176
510
|
if (d.secure) parts.push("Secure");
|
|
177
511
|
if (d.httpOnly) parts.push("HttpOnly");
|
|
178
512
|
parts.push(`SameSite=${d.sameSite}`);
|
|
@@ -180,7 +514,9 @@ function serializeCookie(d) {
|
|
|
180
514
|
}
|
|
181
515
|
async function handleCallback(config, input) {
|
|
182
516
|
const cfg = resolve(config);
|
|
517
|
+
const t0 = Date.now();
|
|
183
518
|
if (!input.code || !input.redirectUri) {
|
|
519
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
184
520
|
return {
|
|
185
521
|
status: 400,
|
|
186
522
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
@@ -188,6 +524,7 @@ async function handleCallback(config, input) {
|
|
|
188
524
|
};
|
|
189
525
|
}
|
|
190
526
|
if (!cfg.secretKey) {
|
|
527
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
191
528
|
return {
|
|
192
529
|
status: 500,
|
|
193
530
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -211,6 +548,7 @@ async function handleCallback(config, input) {
|
|
|
211
548
|
});
|
|
212
549
|
const json = await res.json().catch(() => ({}));
|
|
213
550
|
if (!res.ok || !json.access_token) {
|
|
551
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
214
552
|
return {
|
|
215
553
|
status: res.status || 502,
|
|
216
554
|
body: {
|
|
@@ -230,6 +568,7 @@ async function handleCallback(config, input) {
|
|
|
230
568
|
if (json.refresh_token) {
|
|
231
569
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
232
570
|
}
|
|
571
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
233
572
|
return {
|
|
234
573
|
status: 200,
|
|
235
574
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -238,8 +577,18 @@ async function handleCallback(config, input) {
|
|
|
238
577
|
}
|
|
239
578
|
async function handleRefresh(config, input) {
|
|
240
579
|
const cfg = resolve(config);
|
|
580
|
+
const t0 = Date.now();
|
|
241
581
|
const refreshToken = input.refreshToken;
|
|
582
|
+
const idemKey = input.idempotencyToken;
|
|
583
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
584
|
+
return {
|
|
585
|
+
status: 401,
|
|
586
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
587
|
+
cookies: clearCookies(cfg)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
242
590
|
if (!refreshToken) {
|
|
591
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
243
592
|
return {
|
|
244
593
|
status: 401,
|
|
245
594
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -255,6 +604,7 @@ async function handleRefresh(config, input) {
|
|
|
255
604
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
256
605
|
const status = res.status || 401;
|
|
257
606
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
607
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
258
608
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
259
609
|
cfg.clearCookiesOnRefreshFailure,
|
|
260
610
|
status,
|
|
@@ -278,6 +628,7 @@ async function handleRefresh(config, input) {
|
|
|
278
628
|
if (json.data.refreshToken) {
|
|
279
629
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
280
630
|
}
|
|
631
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
281
632
|
return {
|
|
282
633
|
status: 200,
|
|
283
634
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -286,6 +637,10 @@ async function handleRefresh(config, input) {
|
|
|
286
637
|
}
|
|
287
638
|
async function handleSignout(config, input) {
|
|
288
639
|
const cfg = resolve(config);
|
|
640
|
+
const t0 = Date.now();
|
|
641
|
+
if (input.idempotencyToken) {
|
|
642
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
643
|
+
}
|
|
289
644
|
if (input.accessToken) {
|
|
290
645
|
try {
|
|
291
646
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -307,16 +662,60 @@ async function handleSignout(config, input) {
|
|
|
307
662
|
} catch {
|
|
308
663
|
}
|
|
309
664
|
}
|
|
665
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
310
666
|
return {
|
|
311
667
|
status: 200,
|
|
312
668
|
body: { success: true, data: { signedOut: true } },
|
|
313
669
|
cookies: clearCookies(cfg)
|
|
314
670
|
};
|
|
315
671
|
}
|
|
672
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
673
|
+
function getTokensFor(issuer) {
|
|
674
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
675
|
+
if (!m) {
|
|
676
|
+
m = new TokensModule(issuer);
|
|
677
|
+
TOKENS_CACHE.set(issuer, m);
|
|
678
|
+
}
|
|
679
|
+
return m;
|
|
680
|
+
}
|
|
681
|
+
async function handleUserinfo(config, input) {
|
|
682
|
+
const cfg = resolve(config);
|
|
683
|
+
if (!input.accessToken) {
|
|
684
|
+
return {
|
|
685
|
+
status: 401,
|
|
686
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
687
|
+
cookies: []
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
let claims;
|
|
691
|
+
try {
|
|
692
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
|
|
693
|
+
} catch (err) {
|
|
694
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
695
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
696
|
+
return {
|
|
697
|
+
status: 401,
|
|
698
|
+
body: { success: false, error: { code, message } },
|
|
699
|
+
cookies: []
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
703
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
status: 200,
|
|
707
|
+
body: envelope,
|
|
708
|
+
cookies: []
|
|
709
|
+
};
|
|
710
|
+
}
|
|
316
711
|
// Annotate the CommonJS export names for ESM import in node:
|
|
317
712
|
0 && (module.exports = {
|
|
713
|
+
__resetSignoutMarkersForTests,
|
|
714
|
+
buildUserinfoResponse,
|
|
715
|
+
createInMemorySignoutRegistry,
|
|
318
716
|
handleCallback,
|
|
319
717
|
handleRefresh,
|
|
320
718
|
handleSignout,
|
|
719
|
+
handleUserinfo,
|
|
321
720
|
serializeCookie
|
|
322
721
|
});
|
package/dist/server/handlers.mjs
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
+
__resetSignoutMarkersForTests,
|
|
3
|
+
buildUserinfoResponse,
|
|
4
|
+
createInMemorySignoutRegistry,
|
|
2
5
|
handleCallback,
|
|
3
6
|
handleRefresh,
|
|
4
7
|
handleSignout,
|
|
8
|
+
handleUserinfo,
|
|
5
9
|
serializeCookie
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
10
|
+
} from "../chunk-RUJXRTEW.mjs";
|
|
11
|
+
import "../chunk-HVHNYPDC.mjs";
|
|
12
|
+
import "../chunk-NUO2I65G.mjs";
|
|
13
|
+
import "../chunk-6PJRLRB4.mjs";
|
|
9
14
|
import "../chunk-Y6FXYEAI.mjs";
|
|
10
15
|
export {
|
|
16
|
+
__resetSignoutMarkersForTests,
|
|
17
|
+
buildUserinfoResponse,
|
|
18
|
+
createInMemorySignoutRegistry,
|
|
11
19
|
handleCallback,
|
|
12
20
|
handleRefresh,
|
|
13
21
|
handleSignout,
|
|
22
|
+
handleUserinfo,
|
|
14
23
|
serializeCookie
|
|
15
24
|
};
|