@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/fastify.js
CHANGED
|
@@ -36,13 +36,30 @@ __export(fastify_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(fastify_exports);
|
|
37
37
|
|
|
38
38
|
// src/errors.ts
|
|
39
|
-
var IQAuthError = class extends Error {
|
|
40
|
-
constructor(code, message, status,
|
|
39
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
40
|
+
constructor(code, message, status, cause) {
|
|
41
41
|
super(message);
|
|
42
42
|
this.name = "IQAuthError";
|
|
43
43
|
this.code = code;
|
|
44
44
|
this.status = status;
|
|
45
|
-
this.
|
|
45
|
+
this.cause = cause;
|
|
46
|
+
this.raw = cause;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
50
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
51
|
+
*/
|
|
52
|
+
static isIQAuthError(value) {
|
|
53
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Type-narrowed code check. Lets callers write
|
|
57
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
58
|
+
* taxonomy without losing the ability to handle server codes via
|
|
59
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
60
|
+
*/
|
|
61
|
+
is(code) {
|
|
62
|
+
return this.code === code;
|
|
46
63
|
}
|
|
47
64
|
};
|
|
48
65
|
|
|
@@ -157,7 +174,7 @@ var HttpClient = class {
|
|
|
157
174
|
headers: this.buildHeaders(),
|
|
158
175
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
159
176
|
const refreshToken = this.config.getRefreshToken();
|
|
160
|
-
if (!refreshToken) throw new IQAuthError("
|
|
177
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
161
178
|
return { body: JSON.stringify({ refreshToken }) };
|
|
162
179
|
})()
|
|
163
180
|
});
|
|
@@ -174,7 +191,7 @@ var HttpClient = class {
|
|
|
174
191
|
return;
|
|
175
192
|
}
|
|
176
193
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
177
|
-
throw new IQAuthError("
|
|
194
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
178
195
|
}
|
|
179
196
|
const tokens = {
|
|
180
197
|
accessToken: body.data.accessToken,
|
|
@@ -192,7 +209,7 @@ var HttpClient = class {
|
|
|
192
209
|
return this.requestWithRetry(method, path, body, options, false);
|
|
193
210
|
}
|
|
194
211
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
195
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
212
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
196
213
|
await this.attemptRefresh();
|
|
197
214
|
}
|
|
198
215
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -420,6 +437,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
420
437
|
"iqvalidate"
|
|
421
438
|
];
|
|
422
439
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
440
|
+
function classifyJoseError(err) {
|
|
441
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
442
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
443
|
+
}
|
|
444
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
445
|
+
return { code: "token_invalid", message: err.message };
|
|
446
|
+
}
|
|
447
|
+
if (err instanceof Error) {
|
|
448
|
+
return { code: "token_invalid", message: err.message };
|
|
449
|
+
}
|
|
450
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
451
|
+
}
|
|
423
452
|
function decodeProtectedHeader(token) {
|
|
424
453
|
const parts = token.split(".");
|
|
425
454
|
if (parts.length < 2) return null;
|
|
@@ -456,11 +485,11 @@ var TokensModule = class {
|
|
|
456
485
|
async verify(token, options = {}) {
|
|
457
486
|
const header = decodeProtectedHeader(token);
|
|
458
487
|
if (!header) {
|
|
459
|
-
throw new IQAuthError("
|
|
488
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
460
489
|
}
|
|
461
490
|
const kid = header.kid;
|
|
462
491
|
if (!kid) {
|
|
463
|
-
throw new IQAuthError("
|
|
492
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
464
493
|
}
|
|
465
494
|
let cache = await this.ensureCache();
|
|
466
495
|
if (!cache.byKid.has(kid)) {
|
|
@@ -468,7 +497,7 @@ var TokensModule = class {
|
|
|
468
497
|
cache = await this.ensureCache();
|
|
469
498
|
}
|
|
470
499
|
if (!cache.byKid.has(kid)) {
|
|
471
|
-
throw new IQAuthError("
|
|
500
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
472
501
|
}
|
|
473
502
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
474
503
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -484,16 +513,8 @@ var TokensModule = class {
|
|
|
484
513
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
485
514
|
return payload;
|
|
486
515
|
} catch (err) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
491
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
492
|
-
}
|
|
493
|
-
if (err instanceof Error) {
|
|
494
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
495
|
-
}
|
|
496
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
516
|
+
const classified = classifyJoseError(err);
|
|
517
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
497
518
|
}
|
|
498
519
|
}
|
|
499
520
|
/**
|
|
@@ -535,7 +556,7 @@ var TokensModule = class {
|
|
|
535
556
|
getClaims(token) {
|
|
536
557
|
const claims = this.decode(token);
|
|
537
558
|
if (!claims) {
|
|
538
|
-
throw new IQAuthError("
|
|
559
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
539
560
|
}
|
|
540
561
|
return claims;
|
|
541
562
|
}
|
|
@@ -545,7 +566,7 @@ var TokensModule = class {
|
|
|
545
566
|
}
|
|
546
567
|
await this.refreshJwks();
|
|
547
568
|
if (!this.jwksCache) {
|
|
548
|
-
throw new IQAuthError("
|
|
569
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
549
570
|
}
|
|
550
571
|
return this.jwksCache;
|
|
551
572
|
}
|
|
@@ -555,22 +576,38 @@ var TokensModule = class {
|
|
|
555
576
|
}
|
|
556
577
|
this.inFlightRefresh = (async () => {
|
|
557
578
|
try {
|
|
558
|
-
|
|
579
|
+
let res;
|
|
580
|
+
try {
|
|
581
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
throw new IQAuthError(
|
|
584
|
+
"network",
|
|
585
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
586
|
+
void 0,
|
|
587
|
+
err
|
|
588
|
+
);
|
|
589
|
+
}
|
|
559
590
|
if (!res.ok) {
|
|
560
591
|
throw new IQAuthError(
|
|
561
|
-
"
|
|
562
|
-
`Failed to fetch JWKS: ${res.status}
|
|
592
|
+
"jwks_fetch_failed",
|
|
593
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
594
|
+
res.status
|
|
563
595
|
);
|
|
564
596
|
}
|
|
565
597
|
let jwks;
|
|
566
598
|
try {
|
|
567
599
|
jwks = await res.json();
|
|
568
|
-
} catch {
|
|
569
|
-
throw new IQAuthError(
|
|
600
|
+
} catch (err) {
|
|
601
|
+
throw new IQAuthError(
|
|
602
|
+
"jwks_fetch_failed",
|
|
603
|
+
"Malformed JWKS response: invalid JSON",
|
|
604
|
+
res.status,
|
|
605
|
+
err
|
|
606
|
+
);
|
|
570
607
|
}
|
|
571
608
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
572
609
|
throw new IQAuthError(
|
|
573
|
-
"
|
|
610
|
+
"jwks_fetch_failed",
|
|
574
611
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
575
612
|
);
|
|
576
613
|
}
|
|
@@ -578,7 +615,7 @@ var TokensModule = class {
|
|
|
578
615
|
for (const key of jwks.keys) {
|
|
579
616
|
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")) {
|
|
580
617
|
throw new IQAuthError(
|
|
581
|
-
"
|
|
618
|
+
"jwks_fetch_failed",
|
|
582
619
|
"Malformed JWKS response: key missing required fields"
|
|
583
620
|
);
|
|
584
621
|
}
|
|
@@ -596,6 +633,19 @@ var TokensModule = class {
|
|
|
596
633
|
clearCache() {
|
|
597
634
|
this.jwksCache = null;
|
|
598
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
638
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
639
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
640
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
641
|
+
*/
|
|
642
|
+
async prewarm() {
|
|
643
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
644
|
+
try {
|
|
645
|
+
await this.refreshJwks();
|
|
646
|
+
} catch {
|
|
647
|
+
}
|
|
648
|
+
}
|
|
599
649
|
};
|
|
600
650
|
|
|
601
651
|
// src/modules/sessions.ts
|
|
@@ -919,14 +969,14 @@ var OidcModule = class {
|
|
|
919
969
|
*/
|
|
920
970
|
async handleCallback(params) {
|
|
921
971
|
if (!params.state) {
|
|
922
|
-
throw new IQAuthError("
|
|
972
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
923
973
|
}
|
|
924
974
|
if (!params.code) {
|
|
925
|
-
throw new IQAuthError("
|
|
975
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
926
976
|
}
|
|
927
977
|
const stored = await this.stateStore.get(params.state);
|
|
928
978
|
if (!stored) {
|
|
929
|
-
throw new IQAuthError("
|
|
979
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
930
980
|
}
|
|
931
981
|
let tokens;
|
|
932
982
|
try {
|
|
@@ -944,7 +994,7 @@ var OidcModule = class {
|
|
|
944
994
|
if (tokens.id_token) {
|
|
945
995
|
if (!this.tokensModule) {
|
|
946
996
|
throw new IQAuthError(
|
|
947
|
-
"
|
|
997
|
+
"config_invalid",
|
|
948
998
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
949
999
|
);
|
|
950
1000
|
}
|
|
@@ -955,7 +1005,7 @@ var OidcModule = class {
|
|
|
955
1005
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
956
1006
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
957
1007
|
throw new IQAuthError(
|
|
958
|
-
"
|
|
1008
|
+
"token_invalid",
|
|
959
1009
|
"OIDC id_token nonce did not match the stored value"
|
|
960
1010
|
);
|
|
961
1011
|
}
|
|
@@ -1156,6 +1206,9 @@ var AppsModule = class {
|
|
|
1156
1206
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1157
1207
|
*/
|
|
1158
1208
|
async get(appKey) {
|
|
1209
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1210
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1211
|
+
}
|
|
1159
1212
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1160
1213
|
}
|
|
1161
1214
|
/**
|
|
@@ -1175,6 +1228,16 @@ var AppsModule = class {
|
|
|
1175
1228
|
401
|
|
1176
1229
|
);
|
|
1177
1230
|
}
|
|
1231
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1232
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1233
|
+
}
|
|
1234
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1235
|
+
throw new IQAuthError(
|
|
1236
|
+
"ENVIRONMENT_REQUIRED",
|
|
1237
|
+
"manifest.environment is required and must be 'production', 'staging', or 'development'. This guards against a dev workstation silently overwriting a production app's permission tree.",
|
|
1238
|
+
400
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1178
1241
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1179
1242
|
}
|
|
1180
1243
|
/**
|
|
@@ -1184,11 +1247,14 @@ var AppsModule = class {
|
|
|
1184
1247
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1185
1248
|
*/
|
|
1186
1249
|
async isRegistered(appKey) {
|
|
1250
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1251
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1252
|
+
}
|
|
1187
1253
|
try {
|
|
1188
1254
|
await this.get(appKey);
|
|
1189
1255
|
return true;
|
|
1190
1256
|
} catch (err) {
|
|
1191
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1257
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1192
1258
|
return false;
|
|
1193
1259
|
}
|
|
1194
1260
|
throw err;
|
|
@@ -1225,6 +1291,20 @@ var RolesModule = class {
|
|
|
1225
1291
|
};
|
|
1226
1292
|
|
|
1227
1293
|
// src/modules/permissionGroups.ts
|
|
1294
|
+
function assertAppKey(appKey, callsite) {
|
|
1295
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1296
|
+
throw new IQAuthError(
|
|
1297
|
+
"VALIDATION_ERROR",
|
|
1298
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1299
|
+
400
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1304
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1305
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1228
1308
|
var PermissionGroupsModule = class {
|
|
1229
1309
|
constructor(http) {
|
|
1230
1310
|
this.http = http;
|
|
@@ -1245,7 +1325,14 @@ var PermissionGroupsModule = class {
|
|
|
1245
1325
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1246
1326
|
}
|
|
1247
1327
|
async addPermission(tenantId, groupId, data) {
|
|
1248
|
-
|
|
1328
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1329
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1330
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1331
|
+
appKey: data.appKey,
|
|
1332
|
+
nodeKey: data.nodeKey,
|
|
1333
|
+
effect: data.effect,
|
|
1334
|
+
weight: data.weight
|
|
1335
|
+
});
|
|
1249
1336
|
}
|
|
1250
1337
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1251
1338
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1269,21 +1356,51 @@ var PermissionGroupsModule = class {
|
|
|
1269
1356
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1270
1357
|
}
|
|
1271
1358
|
async addUserOverride(tenantId, userId, data) {
|
|
1272
|
-
|
|
1359
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1360
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1361
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1362
|
+
appKey: data.appKey,
|
|
1363
|
+
nodeKey: data.nodeKey,
|
|
1364
|
+
effect: data.effect,
|
|
1365
|
+
weight: data.weight,
|
|
1366
|
+
expiresAt: data.expiresAt
|
|
1367
|
+
});
|
|
1273
1368
|
}
|
|
1274
1369
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1275
1370
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1276
1371
|
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1374
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1375
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1376
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1377
|
+
*/
|
|
1277
1378
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const qs = query.toString();
|
|
1282
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1379
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1380
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1381
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1283
1382
|
}
|
|
1284
1383
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1384
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1385
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1285
1386
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1286
1387
|
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1390
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1391
|
+
* single misconfigured entry can't slip through and silently report
|
|
1392
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1393
|
+
*/
|
|
1394
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1395
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1396
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1397
|
+
}
|
|
1398
|
+
checks.forEach((c, i) => {
|
|
1399
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1400
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1401
|
+
});
|
|
1402
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1403
|
+
}
|
|
1287
1404
|
};
|
|
1288
1405
|
|
|
1289
1406
|
// src/modules/apiKeys.ts
|
|
@@ -1708,6 +1825,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1708
1825
|
this._refreshToken = tokens.refreshToken;
|
|
1709
1826
|
},
|
|
1710
1827
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1828
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1829
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1830
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1831
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1711
1832
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1712
1833
|
sessionHeaderName: config.sessionHeaderName,
|
|
1713
1834
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1748,6 +1869,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1748
1869
|
static forServer(config) {
|
|
1749
1870
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1750
1871
|
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1874
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1875
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1876
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1877
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1878
|
+
*/
|
|
1751
1879
|
static forMobile(config) {
|
|
1752
1880
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1753
1881
|
}
|
|
@@ -1764,6 +1892,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1764
1892
|
getRefreshToken() {
|
|
1765
1893
|
return this._refreshToken;
|
|
1766
1894
|
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1897
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1898
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1899
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1900
|
+
*/
|
|
1901
|
+
async prewarm() {
|
|
1902
|
+
await Promise.all([
|
|
1903
|
+
this.tokens.prewarm(),
|
|
1904
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1905
|
+
]);
|
|
1906
|
+
}
|
|
1767
1907
|
getCurrentClaims() {
|
|
1768
1908
|
if (!this._accessToken) return null;
|
|
1769
1909
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1804,14 +1944,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1804
1944
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1805
1945
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1806
1946
|
throw new IQAuthError(
|
|
1807
|
-
"
|
|
1947
|
+
"config_invalid",
|
|
1808
1948
|
`${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.`
|
|
1809
1949
|
);
|
|
1810
1950
|
}
|
|
1811
1951
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1812
1952
|
if (!shapeMatch) {
|
|
1813
1953
|
throw new IQAuthError(
|
|
1814
|
-
"
|
|
1954
|
+
"config_invalid",
|
|
1815
1955
|
`${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.`
|
|
1816
1956
|
);
|
|
1817
1957
|
}
|
|
@@ -1820,19 +1960,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1820
1960
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1821
1961
|
} catch {
|
|
1822
1962
|
throw new IQAuthError(
|
|
1823
|
-
"
|
|
1963
|
+
"config_invalid",
|
|
1824
1964
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1825
1965
|
);
|
|
1826
1966
|
}
|
|
1827
1967
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1828
1968
|
throw new IQAuthError(
|
|
1829
|
-
"
|
|
1969
|
+
"config_invalid",
|
|
1830
1970
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1831
1971
|
);
|
|
1832
1972
|
}
|
|
1833
1973
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1834
1974
|
throw new IQAuthError(
|
|
1835
|
-
"
|
|
1975
|
+
"config_invalid",
|
|
1836
1976
|
`${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.`
|
|
1837
1977
|
);
|
|
1838
1978
|
}
|
|
@@ -1845,6 +1985,41 @@ function isPublishableKeyPayload(value) {
|
|
|
1845
1985
|
}
|
|
1846
1986
|
|
|
1847
1987
|
// src/server/handlers.ts
|
|
1988
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
1989
|
+
const baseUser = {
|
|
1990
|
+
sub: claims.sub,
|
|
1991
|
+
email: claims.email,
|
|
1992
|
+
name: claims.name,
|
|
1993
|
+
tenantId: claims.tenantId,
|
|
1994
|
+
vendorId: claims.vendorId,
|
|
1995
|
+
roles: claims.roles ?? [],
|
|
1996
|
+
entitlements: claims.entitlements ?? []
|
|
1997
|
+
};
|
|
1998
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
1999
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
2000
|
+
return {
|
|
2001
|
+
success: true,
|
|
2002
|
+
data: {
|
|
2003
|
+
user,
|
|
2004
|
+
claims,
|
|
2005
|
+
tenantId: claims.tenantId ?? null
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
function emitTiming(cfg, event) {
|
|
2010
|
+
if (cfg.debug) {
|
|
2011
|
+
try {
|
|
2012
|
+
console.debug("[iqauth_helper]", event);
|
|
2013
|
+
} catch {
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
if (cfg.onTimingEvent) {
|
|
2017
|
+
try {
|
|
2018
|
+
cfg.onTimingEvent(event);
|
|
2019
|
+
} catch {
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
1848
2023
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1849
2024
|
"TOKEN_REVOKED",
|
|
1850
2025
|
"SESSION_REVOKED",
|
|
@@ -1884,7 +2059,11 @@ function resolve(config) {
|
|
|
1884
2059
|
})),
|
|
1885
2060
|
appId: parsed.appId,
|
|
1886
2061
|
tenantId: parsed.tenantId,
|
|
1887
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2062
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2063
|
+
debug: config.debug,
|
|
2064
|
+
onTimingEvent: config.onTimingEvent,
|
|
2065
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2066
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
|
|
1888
2067
|
};
|
|
1889
2068
|
}
|
|
1890
2069
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -1901,15 +2080,41 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
1901
2080
|
}
|
|
1902
2081
|
function clearCookies(cfg) {
|
|
1903
2082
|
return [
|
|
1904
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
1905
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
2083
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
2084
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
1906
2085
|
];
|
|
1907
2086
|
}
|
|
2087
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2088
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2089
|
+
function pruneInMemoryMarkers(now) {
|
|
2090
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2091
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2092
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
var defaultSignoutRegistry = {
|
|
2096
|
+
mark(token, ttlMs) {
|
|
2097
|
+
const now = Date.now();
|
|
2098
|
+
pruneInMemoryMarkers(now);
|
|
2099
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2100
|
+
},
|
|
2101
|
+
has(token) {
|
|
2102
|
+
const now = Date.now();
|
|
2103
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2104
|
+
if (!exp) return false;
|
|
2105
|
+
if (exp <= now) {
|
|
2106
|
+
inMemorySignoutMarkers.delete(token);
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
return true;
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
1908
2112
|
function serializeCookie(d) {
|
|
1909
2113
|
const parts = [`${d.name}=${encodeURIComponent(d.value)}`];
|
|
1910
2114
|
parts.push(`Path=${d.path}`);
|
|
1911
2115
|
if (d.domain) parts.push(`Domain=${d.domain}`);
|
|
1912
2116
|
parts.push(`Max-Age=${d.maxAge}`);
|
|
2117
|
+
if (d.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
1913
2118
|
if (d.secure) parts.push("Secure");
|
|
1914
2119
|
if (d.httpOnly) parts.push("HttpOnly");
|
|
1915
2120
|
parts.push(`SameSite=${d.sameSite}`);
|
|
@@ -1917,7 +2122,9 @@ function serializeCookie(d) {
|
|
|
1917
2122
|
}
|
|
1918
2123
|
async function handleCallback(config, input) {
|
|
1919
2124
|
const cfg = resolve(config);
|
|
2125
|
+
const t0 = Date.now();
|
|
1920
2126
|
if (!input.code || !input.redirectUri) {
|
|
2127
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
1921
2128
|
return {
|
|
1922
2129
|
status: 400,
|
|
1923
2130
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
@@ -1925,6 +2132,7 @@ async function handleCallback(config, input) {
|
|
|
1925
2132
|
};
|
|
1926
2133
|
}
|
|
1927
2134
|
if (!cfg.secretKey) {
|
|
2135
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
1928
2136
|
return {
|
|
1929
2137
|
status: 500,
|
|
1930
2138
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -1948,6 +2156,7 @@ async function handleCallback(config, input) {
|
|
|
1948
2156
|
});
|
|
1949
2157
|
const json = await res.json().catch(() => ({}));
|
|
1950
2158
|
if (!res.ok || !json.access_token) {
|
|
2159
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
1951
2160
|
return {
|
|
1952
2161
|
status: res.status || 502,
|
|
1953
2162
|
body: {
|
|
@@ -1967,6 +2176,7 @@ async function handleCallback(config, input) {
|
|
|
1967
2176
|
if (json.refresh_token) {
|
|
1968
2177
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
1969
2178
|
}
|
|
2179
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
1970
2180
|
return {
|
|
1971
2181
|
status: 200,
|
|
1972
2182
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -1975,8 +2185,18 @@ async function handleCallback(config, input) {
|
|
|
1975
2185
|
}
|
|
1976
2186
|
async function handleRefresh(config, input) {
|
|
1977
2187
|
const cfg = resolve(config);
|
|
2188
|
+
const t0 = Date.now();
|
|
1978
2189
|
const refreshToken = input.refreshToken;
|
|
2190
|
+
const idemKey = input.idempotencyToken;
|
|
2191
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
2192
|
+
return {
|
|
2193
|
+
status: 401,
|
|
2194
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
2195
|
+
cookies: clearCookies(cfg)
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
1979
2198
|
if (!refreshToken) {
|
|
2199
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
1980
2200
|
return {
|
|
1981
2201
|
status: 401,
|
|
1982
2202
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -1992,6 +2212,7 @@ async function handleRefresh(config, input) {
|
|
|
1992
2212
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
1993
2213
|
const status = res.status || 401;
|
|
1994
2214
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2215
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
1995
2216
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
1996
2217
|
cfg.clearCookiesOnRefreshFailure,
|
|
1997
2218
|
status,
|
|
@@ -2015,6 +2236,7 @@ async function handleRefresh(config, input) {
|
|
|
2015
2236
|
if (json.data.refreshToken) {
|
|
2016
2237
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
2017
2238
|
}
|
|
2239
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
2018
2240
|
return {
|
|
2019
2241
|
status: 200,
|
|
2020
2242
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -2023,6 +2245,10 @@ async function handleRefresh(config, input) {
|
|
|
2023
2245
|
}
|
|
2024
2246
|
async function handleSignout(config, input) {
|
|
2025
2247
|
const cfg = resolve(config);
|
|
2248
|
+
const t0 = Date.now();
|
|
2249
|
+
if (input.idempotencyToken) {
|
|
2250
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
2251
|
+
}
|
|
2026
2252
|
if (input.accessToken) {
|
|
2027
2253
|
try {
|
|
2028
2254
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -2044,12 +2270,52 @@ async function handleSignout(config, input) {
|
|
|
2044
2270
|
} catch {
|
|
2045
2271
|
}
|
|
2046
2272
|
}
|
|
2273
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
2047
2274
|
return {
|
|
2048
2275
|
status: 200,
|
|
2049
2276
|
body: { success: true, data: { signedOut: true } },
|
|
2050
2277
|
cookies: clearCookies(cfg)
|
|
2051
2278
|
};
|
|
2052
2279
|
}
|
|
2280
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2281
|
+
function getTokensFor(issuer) {
|
|
2282
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2283
|
+
if (!m) {
|
|
2284
|
+
m = new TokensModule(issuer);
|
|
2285
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2286
|
+
}
|
|
2287
|
+
return m;
|
|
2288
|
+
}
|
|
2289
|
+
async function handleUserinfo(config, input) {
|
|
2290
|
+
const cfg = resolve(config);
|
|
2291
|
+
if (!input.accessToken) {
|
|
2292
|
+
return {
|
|
2293
|
+
status: 401,
|
|
2294
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2295
|
+
cookies: []
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
let claims;
|
|
2299
|
+
try {
|
|
2300
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
|
|
2301
|
+
} catch (err) {
|
|
2302
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2303
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2304
|
+
return {
|
|
2305
|
+
status: 401,
|
|
2306
|
+
body: { success: false, error: { code, message } },
|
|
2307
|
+
cookies: []
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2311
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2312
|
+
});
|
|
2313
|
+
return {
|
|
2314
|
+
status: 200,
|
|
2315
|
+
body: envelope,
|
|
2316
|
+
cookies: []
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2053
2319
|
|
|
2054
2320
|
// src/fastify.ts
|
|
2055
2321
|
var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
|
|
@@ -2058,7 +2324,10 @@ var KNOWN_AUTH_ERRORS = /* @__PURE__ */ new Set([
|
|
|
2058
2324
|
"TOKEN_REVOKED",
|
|
2059
2325
|
"SESSION_EXPIRED",
|
|
2060
2326
|
"SESSION_INVALID",
|
|
2061
|
-
"AUTH_REQUIRED"
|
|
2327
|
+
"AUTH_REQUIRED",
|
|
2328
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy.
|
|
2329
|
+
"token_invalid",
|
|
2330
|
+
"token_expired"
|
|
2062
2331
|
]);
|
|
2063
2332
|
function applyResponse(reply, hr) {
|
|
2064
2333
|
for (const c of hr.cookies) {
|
|
@@ -2107,6 +2376,7 @@ async function iqAuth(fastify, options) {
|
|
|
2107
2376
|
const mountHelpers = options.mountHelperRoutes !== false;
|
|
2108
2377
|
const isPublic = (p) => {
|
|
2109
2378
|
if (mountHelpers && p.startsWith(mount + "/")) return true;
|
|
2379
|
+
if (options.mountUserinfo && p === `${mount}/me`) return true;
|
|
2110
2380
|
if (Array.isArray(options.publicPaths)) return options.publicPaths.includes(p);
|
|
2111
2381
|
if (typeof options.publicPaths === "function") return options.publicPaths(p);
|
|
2112
2382
|
return false;
|
|
@@ -2145,13 +2415,23 @@ async function iqAuth(fastify, options) {
|
|
|
2145
2415
|
fastify.post(`${mount}/refresh`, async (req, reply) => {
|
|
2146
2416
|
const body = req.body || {};
|
|
2147
2417
|
const refreshToken = body.refreshToken || readCookie(req, refreshCookie);
|
|
2148
|
-
|
|
2418
|
+
const idempotencyToken = req.headers?.["x-iqauth-idempotency"] || body.idempotencyToken;
|
|
2419
|
+
applyResponse(reply, await handleRefresh(helperConfig, { refreshToken, idempotencyToken }));
|
|
2149
2420
|
});
|
|
2150
2421
|
fastify.post(`${mount}/signout`, async (req, reply) => {
|
|
2151
2422
|
const auth = req.headers?.authorization;
|
|
2152
2423
|
const accessToken = (typeof auth === "string" ? auth.replace(/^Bearer /i, "") : void 0) || readCookie(req, accessCookie);
|
|
2424
|
+
const refreshToken = readCookie(req, refreshCookie);
|
|
2153
2425
|
const ssoCookieHeader = typeof req.headers?.cookie === "string" ? req.headers.cookie : void 0;
|
|
2154
|
-
|
|
2426
|
+
const idempotencyToken = req.headers?.["x-iqauth-idempotency"];
|
|
2427
|
+
applyResponse(reply, await handleSignout(helperConfig, { accessToken, refreshToken, idempotencyToken, ssoCookieHeader }));
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
if (options.mountUserinfo) {
|
|
2431
|
+
fastify.get(`${mount}/me`, async (req, reply) => {
|
|
2432
|
+
const auth = req.headers?.authorization;
|
|
2433
|
+
const accessToken = (typeof auth === "string" ? auth.replace(/^Bearer /i, "") : void 0) || readCookie(req, accessCookie);
|
|
2434
|
+
applyResponse(reply, await handleUserinfo(helperConfig, { accessToken, req }));
|
|
2155
2435
|
});
|
|
2156
2436
|
}
|
|
2157
2437
|
fastify.decorate("iqauth", { client, issuer });
|