@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/express.js
CHANGED
|
@@ -38,13 +38,30 @@ __export(express_exports, {
|
|
|
38
38
|
module.exports = __toCommonJS(express_exports);
|
|
39
39
|
|
|
40
40
|
// src/errors.ts
|
|
41
|
-
var IQAuthError = class extends Error {
|
|
42
|
-
constructor(code, message, status,
|
|
41
|
+
var IQAuthError = class _IQAuthError extends Error {
|
|
42
|
+
constructor(code, message, status, cause) {
|
|
43
43
|
super(message);
|
|
44
44
|
this.name = "IQAuthError";
|
|
45
45
|
this.code = code;
|
|
46
46
|
this.status = status;
|
|
47
|
-
this.
|
|
47
|
+
this.cause = cause;
|
|
48
|
+
this.raw = cause;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Type guard: true when `value` is an `IQAuthError`. Useful for adapters
|
|
52
|
+
* that round-trip errors through `unknown` (e.g. fastify's `setErrorHandler`).
|
|
53
|
+
*/
|
|
54
|
+
static isIQAuthError(value) {
|
|
55
|
+
return value instanceof _IQAuthError || typeof value === "object" && value !== null && value.name === "IQAuthError" && typeof value.code === "string";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Type-narrowed code check. Lets callers write
|
|
59
|
+
* `if (err.is("token_expired")) …` with full IntelliSense for the typed
|
|
60
|
+
* taxonomy without losing the ability to handle server codes via
|
|
61
|
+
* `err.code === "TOKEN_REVOKED"`.
|
|
62
|
+
*/
|
|
63
|
+
is(code) {
|
|
64
|
+
return this.code === code;
|
|
48
65
|
}
|
|
49
66
|
};
|
|
50
67
|
var ErrorCodes = {
|
|
@@ -195,7 +212,7 @@ var HttpClient = class {
|
|
|
195
212
|
headers: this.buildHeaders(),
|
|
196
213
|
...this.isBrowserSession() ? { credentials: "include" } : (() => {
|
|
197
214
|
const refreshToken = this.config.getRefreshToken();
|
|
198
|
-
if (!refreshToken) throw new IQAuthError("
|
|
215
|
+
if (!refreshToken) throw new IQAuthError("config_invalid", "No refresh token available");
|
|
199
216
|
return { body: JSON.stringify({ refreshToken }) };
|
|
200
217
|
})()
|
|
201
218
|
});
|
|
@@ -212,7 +229,7 @@ var HttpClient = class {
|
|
|
212
229
|
return;
|
|
213
230
|
}
|
|
214
231
|
if (!body.data.accessToken || !body.data.refreshToken) {
|
|
215
|
-
throw new IQAuthError("
|
|
232
|
+
throw new IQAuthError("token_invalid", "Refresh response did not include a token pair");
|
|
216
233
|
}
|
|
217
234
|
const tokens = {
|
|
218
235
|
accessToken: body.data.accessToken,
|
|
@@ -230,7 +247,7 @@ var HttpClient = class {
|
|
|
230
247
|
return this.requestWithRetry(method, path, body, options, false);
|
|
231
248
|
}
|
|
232
249
|
async requestWithRetry(method, path, body, options, hasRetried) {
|
|
233
|
-
if (this.config.autoRefresh && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
250
|
+
if (this.config.autoRefresh && this.config.proactiveRefresh !== false && !options?.skipAutoRefresh && !this.isBrowserSession() && this.config.getRefreshToken() && this.isTokenExpiringSoon()) {
|
|
234
251
|
await this.attemptRefresh();
|
|
235
252
|
}
|
|
236
253
|
const url = `${this.config.baseUrl}${path}`;
|
|
@@ -458,6 +475,18 @@ var DEFAULT_TOKEN_AUDIENCE = [
|
|
|
458
475
|
"iqvalidate"
|
|
459
476
|
];
|
|
460
477
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
478
|
+
function classifyJoseError(err) {
|
|
479
|
+
if (err instanceof import_jose.errors.JWTExpired) {
|
|
480
|
+
return { code: "token_expired", message: "Token has expired" };
|
|
481
|
+
}
|
|
482
|
+
if (err instanceof import_jose.errors.JOSEError) {
|
|
483
|
+
return { code: "token_invalid", message: err.message };
|
|
484
|
+
}
|
|
485
|
+
if (err instanceof Error) {
|
|
486
|
+
return { code: "token_invalid", message: err.message };
|
|
487
|
+
}
|
|
488
|
+
return { code: "token_invalid", message: "Token verification failed" };
|
|
489
|
+
}
|
|
461
490
|
function decodeProtectedHeader(token) {
|
|
462
491
|
const parts = token.split(".");
|
|
463
492
|
if (parts.length < 2) return null;
|
|
@@ -494,11 +523,11 @@ var TokensModule = class {
|
|
|
494
523
|
async verify(token, options = {}) {
|
|
495
524
|
const header = decodeProtectedHeader(token);
|
|
496
525
|
if (!header) {
|
|
497
|
-
throw new IQAuthError("
|
|
526
|
+
throw new IQAuthError("token_invalid", "Unable to decode token");
|
|
498
527
|
}
|
|
499
528
|
const kid = header.kid;
|
|
500
529
|
if (!kid) {
|
|
501
|
-
throw new IQAuthError("
|
|
530
|
+
throw new IQAuthError("token_invalid", "Token missing kid header");
|
|
502
531
|
}
|
|
503
532
|
let cache = await this.ensureCache();
|
|
504
533
|
if (!cache.byKid.has(kid)) {
|
|
@@ -506,7 +535,7 @@ var TokensModule = class {
|
|
|
506
535
|
cache = await this.ensureCache();
|
|
507
536
|
}
|
|
508
537
|
if (!cache.byKid.has(kid)) {
|
|
509
|
-
throw new IQAuthError("
|
|
538
|
+
throw new IQAuthError("token_invalid", `Unknown key ID: ${kid}`);
|
|
510
539
|
}
|
|
511
540
|
const issuer = options.issuer ?? this.defaultIssuer;
|
|
512
541
|
const audience = options.audience ?? this.defaultAudience;
|
|
@@ -522,16 +551,8 @@ var TokensModule = class {
|
|
|
522
551
|
const { payload } = await (0, import_jose.jwtVerify)(token, cache.verifier, verifyOptions);
|
|
523
552
|
return payload;
|
|
524
553
|
} catch (err) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
if (err instanceof import_jose.errors.JOSEError) {
|
|
529
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
530
|
-
}
|
|
531
|
-
if (err instanceof Error) {
|
|
532
|
-
throw new IQAuthError("TOKEN_INVALID", err.message);
|
|
533
|
-
}
|
|
534
|
-
throw new IQAuthError("TOKEN_INVALID", "Token verification failed");
|
|
554
|
+
const classified = classifyJoseError(err);
|
|
555
|
+
throw new IQAuthError(classified.code, classified.message, void 0, err);
|
|
535
556
|
}
|
|
536
557
|
}
|
|
537
558
|
/**
|
|
@@ -573,7 +594,7 @@ var TokensModule = class {
|
|
|
573
594
|
getClaims(token) {
|
|
574
595
|
const claims = this.decode(token);
|
|
575
596
|
if (!claims) {
|
|
576
|
-
throw new IQAuthError("
|
|
597
|
+
throw new IQAuthError("token_invalid", "Unable to decode token claims");
|
|
577
598
|
}
|
|
578
599
|
return claims;
|
|
579
600
|
}
|
|
@@ -583,7 +604,7 @@ var TokensModule = class {
|
|
|
583
604
|
}
|
|
584
605
|
await this.refreshJwks();
|
|
585
606
|
if (!this.jwksCache) {
|
|
586
|
-
throw new IQAuthError("
|
|
607
|
+
throw new IQAuthError("jwks_unavailable", "JWKS cache unavailable after refresh");
|
|
587
608
|
}
|
|
588
609
|
return this.jwksCache;
|
|
589
610
|
}
|
|
@@ -593,22 +614,38 @@ var TokensModule = class {
|
|
|
593
614
|
}
|
|
594
615
|
this.inFlightRefresh = (async () => {
|
|
595
616
|
try {
|
|
596
|
-
|
|
617
|
+
let res;
|
|
618
|
+
try {
|
|
619
|
+
res = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
|
|
620
|
+
} catch (err) {
|
|
621
|
+
throw new IQAuthError(
|
|
622
|
+
"network",
|
|
623
|
+
err instanceof Error ? err.message : "JWKS fetch network error",
|
|
624
|
+
void 0,
|
|
625
|
+
err
|
|
626
|
+
);
|
|
627
|
+
}
|
|
597
628
|
if (!res.ok) {
|
|
598
629
|
throw new IQAuthError(
|
|
599
|
-
"
|
|
600
|
-
`Failed to fetch JWKS: ${res.status}
|
|
630
|
+
"jwks_fetch_failed",
|
|
631
|
+
`Failed to fetch JWKS: ${res.status}`,
|
|
632
|
+
res.status
|
|
601
633
|
);
|
|
602
634
|
}
|
|
603
635
|
let jwks;
|
|
604
636
|
try {
|
|
605
637
|
jwks = await res.json();
|
|
606
|
-
} catch {
|
|
607
|
-
throw new IQAuthError(
|
|
638
|
+
} catch (err) {
|
|
639
|
+
throw new IQAuthError(
|
|
640
|
+
"jwks_fetch_failed",
|
|
641
|
+
"Malformed JWKS response: invalid JSON",
|
|
642
|
+
res.status,
|
|
643
|
+
err
|
|
644
|
+
);
|
|
608
645
|
}
|
|
609
646
|
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
610
647
|
throw new IQAuthError(
|
|
611
|
-
"
|
|
648
|
+
"jwks_fetch_failed",
|
|
612
649
|
"Malformed JWKS response: expected { keys: [...] }"
|
|
613
650
|
);
|
|
614
651
|
}
|
|
@@ -616,7 +653,7 @@ var TokensModule = class {
|
|
|
616
653
|
for (const key of jwks.keys) {
|
|
617
654
|
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")) {
|
|
618
655
|
throw new IQAuthError(
|
|
619
|
-
"
|
|
656
|
+
"jwks_fetch_failed",
|
|
620
657
|
"Malformed JWKS response: key missing required fields"
|
|
621
658
|
);
|
|
622
659
|
}
|
|
@@ -634,6 +671,19 @@ var TokensModule = class {
|
|
|
634
671
|
clearCache() {
|
|
635
672
|
this.jwksCache = null;
|
|
636
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Task #126: Eagerly populate the JWKS cache so the first verify() call
|
|
676
|
+
* doesn't pay a network round-trip. Safe to call repeatedly — single-flight
|
|
677
|
+
* behavior is shared with the lazy refresh path. Errors are swallowed so
|
|
678
|
+
* callers (e.g. `attachHelpers` auto-prewarm) can fire-and-forget.
|
|
679
|
+
*/
|
|
680
|
+
async prewarm() {
|
|
681
|
+
if (this.jwksCache && Date.now() - this.jwksCache.fetchedAt <= JWKS_CACHE_TTL_MS) return;
|
|
682
|
+
try {
|
|
683
|
+
await this.refreshJwks();
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
}
|
|
637
687
|
};
|
|
638
688
|
|
|
639
689
|
// src/modules/sessions.ts
|
|
@@ -957,14 +1007,14 @@ var OidcModule = class {
|
|
|
957
1007
|
*/
|
|
958
1008
|
async handleCallback(params) {
|
|
959
1009
|
if (!params.state) {
|
|
960
|
-
throw new IQAuthError("
|
|
1010
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing state parameter");
|
|
961
1011
|
}
|
|
962
1012
|
if (!params.code) {
|
|
963
|
-
throw new IQAuthError("
|
|
1013
|
+
throw new IQAuthError("config_invalid", "OIDC callback missing code parameter");
|
|
964
1014
|
}
|
|
965
1015
|
const stored = await this.stateStore.get(params.state);
|
|
966
1016
|
if (!stored) {
|
|
967
|
-
throw new IQAuthError("
|
|
1017
|
+
throw new IQAuthError("config_invalid", "Unknown or expired OIDC state");
|
|
968
1018
|
}
|
|
969
1019
|
let tokens;
|
|
970
1020
|
try {
|
|
@@ -982,7 +1032,7 @@ var OidcModule = class {
|
|
|
982
1032
|
if (tokens.id_token) {
|
|
983
1033
|
if (!this.tokensModule) {
|
|
984
1034
|
throw new IQAuthError(
|
|
985
|
-
"
|
|
1035
|
+
"config_invalid",
|
|
986
1036
|
"OIDC handleCallback received an id_token but no TokensModule is configured for verification"
|
|
987
1037
|
);
|
|
988
1038
|
}
|
|
@@ -993,7 +1043,7 @@ var OidcModule = class {
|
|
|
993
1043
|
const tokenNonce = typeof claimsBag.nonce === "string" ? claimsBag.nonce : void 0;
|
|
994
1044
|
if (!tokenNonce || tokenNonce !== stored.nonce) {
|
|
995
1045
|
throw new IQAuthError(
|
|
996
|
-
"
|
|
1046
|
+
"token_invalid",
|
|
997
1047
|
"OIDC id_token nonce did not match the stored value"
|
|
998
1048
|
);
|
|
999
1049
|
}
|
|
@@ -1194,6 +1244,9 @@ var AppsModule = class {
|
|
|
1194
1244
|
* @remarks Wraps GET /api/v1/apps/:appKey
|
|
1195
1245
|
*/
|
|
1196
1246
|
async get(appKey) {
|
|
1247
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1248
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1249
|
+
}
|
|
1197
1250
|
return this.http.request("GET", `/api/v1/apps/${encodeURIComponent(appKey)}`);
|
|
1198
1251
|
}
|
|
1199
1252
|
/**
|
|
@@ -1213,6 +1266,16 @@ var AppsModule = class {
|
|
|
1213
1266
|
401
|
|
1214
1267
|
);
|
|
1215
1268
|
}
|
|
1269
|
+
if (!manifest || typeof manifest.key !== "string" || manifest.key.trim() === "") {
|
|
1270
|
+
throw new IQAuthError("VALIDATION_ERROR", "manifest.key (appKey) is required for register().", 400);
|
|
1271
|
+
}
|
|
1272
|
+
if (!manifest.environment || !["production", "staging", "development"].includes(manifest.environment)) {
|
|
1273
|
+
throw new IQAuthError(
|
|
1274
|
+
"ENVIRONMENT_REQUIRED",
|
|
1275
|
+
"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.",
|
|
1276
|
+
400
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1216
1279
|
return this.http.request("POST", "/api/v1/apps/sync", manifest);
|
|
1217
1280
|
}
|
|
1218
1281
|
/**
|
|
@@ -1222,11 +1285,14 @@ var AppsModule = class {
|
|
|
1222
1285
|
* @remarks Uses GET /api/v1/apps/:appKey — catches 404 errors
|
|
1223
1286
|
*/
|
|
1224
1287
|
async isRegistered(appKey) {
|
|
1288
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1289
|
+
throw new IQAuthError("VALIDATION_ERROR", "appKey is required (no env-var fallback).", 400);
|
|
1290
|
+
}
|
|
1225
1291
|
try {
|
|
1226
1292
|
await this.get(appKey);
|
|
1227
1293
|
return true;
|
|
1228
1294
|
} catch (err) {
|
|
1229
|
-
if (err.code === "NOT_FOUND" || err.status === 404) {
|
|
1295
|
+
if (err.code === "app_not_found" || err.code === "NOT_FOUND" || err.status === 404) {
|
|
1230
1296
|
return false;
|
|
1231
1297
|
}
|
|
1232
1298
|
throw err;
|
|
@@ -1263,6 +1329,20 @@ var RolesModule = class {
|
|
|
1263
1329
|
};
|
|
1264
1330
|
|
|
1265
1331
|
// src/modules/permissionGroups.ts
|
|
1332
|
+
function assertAppKey(appKey, callsite) {
|
|
1333
|
+
if (typeof appKey !== "string" || appKey.trim() === "") {
|
|
1334
|
+
throw new IQAuthError(
|
|
1335
|
+
"VALIDATION_ERROR",
|
|
1336
|
+
`appKey is required for ${callsite} (no env-var fallback, no 'product' alias).`,
|
|
1337
|
+
400
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function assertNodeKey(nodeKey, callsite) {
|
|
1342
|
+
if (typeof nodeKey !== "string" || nodeKey.trim() === "") {
|
|
1343
|
+
throw new IQAuthError("VALIDATION_ERROR", `nodeKey is required for ${callsite}.`, 400);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1266
1346
|
var PermissionGroupsModule = class {
|
|
1267
1347
|
constructor(http) {
|
|
1268
1348
|
this.http = http;
|
|
@@ -1283,7 +1363,14 @@ var PermissionGroupsModule = class {
|
|
|
1283
1363
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`);
|
|
1284
1364
|
}
|
|
1285
1365
|
async addPermission(tenantId, groupId, data) {
|
|
1286
|
-
|
|
1366
|
+
assertAppKey(data?.appKey, "permissionGroups.addPermission");
|
|
1367
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addPermission");
|
|
1368
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions`, {
|
|
1369
|
+
appKey: data.appKey,
|
|
1370
|
+
nodeKey: data.nodeKey,
|
|
1371
|
+
effect: data.effect,
|
|
1372
|
+
weight: data.weight
|
|
1373
|
+
});
|
|
1287
1374
|
}
|
|
1288
1375
|
async removePermission(tenantId, groupId, permissionId) {
|
|
1289
1376
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/permission-groups/${groupId}/permissions/${permissionId}`);
|
|
@@ -1307,21 +1394,51 @@ var PermissionGroupsModule = class {
|
|
|
1307
1394
|
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`);
|
|
1308
1395
|
}
|
|
1309
1396
|
async addUserOverride(tenantId, userId, data) {
|
|
1310
|
-
|
|
1397
|
+
assertAppKey(data?.appKey, "permissionGroups.addUserOverride");
|
|
1398
|
+
assertNodeKey(data?.nodeKey, "permissionGroups.addUserOverride");
|
|
1399
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides`, {
|
|
1400
|
+
appKey: data.appKey,
|
|
1401
|
+
nodeKey: data.nodeKey,
|
|
1402
|
+
effect: data.effect,
|
|
1403
|
+
weight: data.weight,
|
|
1404
|
+
expiresAt: data.expiresAt
|
|
1405
|
+
});
|
|
1311
1406
|
}
|
|
1312
1407
|
async removeUserOverride(tenantId, userId, overrideId) {
|
|
1313
1408
|
return this.http.request("DELETE", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/overrides/${overrideId}`);
|
|
1314
1409
|
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Task #130 — `appKey` is REQUIRED. The legacy `product` query alias is no
|
|
1412
|
+
* longer accepted at the SDK boundary; pass it as `appKey` instead. The
|
|
1413
|
+
* server still accepts `product=` from raw HTTP callers during the
|
|
1414
|
+
* deprecation window, but the SDK will not silently translate it.
|
|
1415
|
+
*/
|
|
1315
1416
|
async getEffectivePermissions(tenantId, userId, params) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
const qs = query.toString();
|
|
1320
|
-
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective${qs ? `?${qs}` : ""}`);
|
|
1417
|
+
assertAppKey(params?.appKey, "permissionGroups.getEffectivePermissions");
|
|
1418
|
+
const qs = new URLSearchParams({ appKey: params.appKey }).toString();
|
|
1419
|
+
return this.http.request("GET", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/effective?${qs}`);
|
|
1321
1420
|
}
|
|
1322
1421
|
async checkPermission(tenantId, userId, appKey, nodeKey) {
|
|
1422
|
+
assertAppKey(appKey, "permissionGroups.checkPermission");
|
|
1423
|
+
assertNodeKey(nodeKey, "permissionGroups.checkPermission");
|
|
1323
1424
|
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/check`, { appKey, nodeKey });
|
|
1324
1425
|
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Task #130 — every entry in `checks` must include a non-empty `appKey`
|
|
1428
|
+
* AND `nodeKey`. The SDK validates the whole batch before sending so a
|
|
1429
|
+
* single misconfigured entry can't slip through and silently report
|
|
1430
|
+
* `allowed: false` from the server's per-entry validation branch.
|
|
1431
|
+
*/
|
|
1432
|
+
async batchCheckPermissions(tenantId, userId, checks) {
|
|
1433
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
1434
|
+
throw new IQAuthError("VALIDATION_ERROR", "checks must be a non-empty array of { appKey, nodeKey }.", 400);
|
|
1435
|
+
}
|
|
1436
|
+
checks.forEach((c, i) => {
|
|
1437
|
+
assertAppKey(c?.appKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1438
|
+
assertNodeKey(c?.nodeKey, `permissionGroups.batchCheckPermissions[${i}]`);
|
|
1439
|
+
});
|
|
1440
|
+
return this.http.request("POST", `/api/v1/tenants/${tenantId}/users/${userId}/permissions/batch-check`, { checks });
|
|
1441
|
+
}
|
|
1325
1442
|
};
|
|
1326
1443
|
|
|
1327
1444
|
// src/modules/apiKeys.ts
|
|
@@ -1746,6 +1863,10 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1746
1863
|
this._refreshToken = tokens.refreshToken;
|
|
1747
1864
|
},
|
|
1748
1865
|
autoRefresh: "autoRefresh" in config ? config.autoRefresh !== false : true,
|
|
1866
|
+
// `'app-state'` is mobile-only — on any other environment we treat it
|
|
1867
|
+
// as the default `true` (proactive refresh ON). Only the mobile client
|
|
1868
|
+
// disables proactive refresh and replaces it with an AppState listener.
|
|
1869
|
+
proactiveRefresh: "autoRefresh" in config && config.autoRefresh === "app-state" && _IQAuthClient.resolveEnvironment(config) === "mobile" ? false : true,
|
|
1749
1870
|
onTokenRefresh: "onTokenRefresh" in config ? config.onTokenRefresh : void 0,
|
|
1750
1871
|
sessionHeaderName: config.sessionHeaderName,
|
|
1751
1872
|
sessionHeaderValue: config.sessionHeaderValue,
|
|
@@ -1786,6 +1907,13 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1786
1907
|
static forServer(config) {
|
|
1787
1908
|
return new _IQAuthClient({ ...config, environment: "server" });
|
|
1788
1909
|
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Construct a mobile-environment client. NOTE: this constructor does NOT
|
|
1912
|
+
* subscribe to React Native's `AppState` even when `autoRefresh: 'app-state'`
|
|
1913
|
+
* is passed — it only disables the per-request proactive refresh. Use
|
|
1914
|
+
* `createMobileClient` from `@iqauth/sdk/mobile` if you want the full
|
|
1915
|
+
* AppState-driven refresh behavior (recommended for Expo / React Native).
|
|
1916
|
+
*/
|
|
1789
1917
|
static forMobile(config) {
|
|
1790
1918
|
return new _IQAuthClient({ ...config, environment: "mobile" });
|
|
1791
1919
|
}
|
|
@@ -1802,6 +1930,18 @@ var IQAuthClient = class _IQAuthClient {
|
|
|
1802
1930
|
getRefreshToken() {
|
|
1803
1931
|
return this._refreshToken;
|
|
1804
1932
|
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Task #126: Eagerly fetch JWKS + OIDC discovery so the first verify() /
|
|
1935
|
+
* refresh round-trip on the request hot path doesn't pay the discovery
|
|
1936
|
+
* fetch latency. Safe to call repeatedly. Errors are swallowed; callers
|
|
1937
|
+
* may fire-and-forget. Called automatically by `iqAuth({...}).attachHelpers()`.
|
|
1938
|
+
*/
|
|
1939
|
+
async prewarm() {
|
|
1940
|
+
await Promise.all([
|
|
1941
|
+
this.tokens.prewarm(),
|
|
1942
|
+
this.oidc.getDiscovery().catch(() => void 0)
|
|
1943
|
+
]);
|
|
1944
|
+
}
|
|
1805
1945
|
getCurrentClaims() {
|
|
1806
1946
|
if (!this._accessToken) return null;
|
|
1807
1947
|
return this.tokens.decode(this._accessToken);
|
|
@@ -1842,14 +1982,14 @@ function assertPublishableKey(raw, opts) {
|
|
|
1842
1982
|
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
1843
1983
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
1844
1984
|
throw new IQAuthError(
|
|
1845
|
-
"
|
|
1985
|
+
"config_invalid",
|
|
1846
1986
|
`${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.`
|
|
1847
1987
|
);
|
|
1848
1988
|
}
|
|
1849
1989
|
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
1850
1990
|
if (!shapeMatch) {
|
|
1851
1991
|
throw new IQAuthError(
|
|
1852
|
-
"
|
|
1992
|
+
"config_invalid",
|
|
1853
1993
|
`${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.`
|
|
1854
1994
|
);
|
|
1855
1995
|
}
|
|
@@ -1858,19 +1998,19 @@ function assertPublishableKey(raw, opts) {
|
|
|
1858
1998
|
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
1859
1999
|
} catch {
|
|
1860
2000
|
throw new IQAuthError(
|
|
1861
|
-
"
|
|
2001
|
+
"config_invalid",
|
|
1862
2002
|
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
1863
2003
|
);
|
|
1864
2004
|
}
|
|
1865
2005
|
if (!isPublishableKeyPayload(decoded)) {
|
|
1866
2006
|
throw new IQAuthError(
|
|
1867
|
-
"
|
|
2007
|
+
"config_invalid",
|
|
1868
2008
|
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
1869
2009
|
);
|
|
1870
2010
|
}
|
|
1871
2011
|
if (!isValidIssuerUrl(decoded.iss)) {
|
|
1872
2012
|
throw new IQAuthError(
|
|
1873
|
-
"
|
|
2013
|
+
"config_invalid",
|
|
1874
2014
|
`${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.`
|
|
1875
2015
|
);
|
|
1876
2016
|
}
|
|
@@ -1884,12 +2024,18 @@ function isPublishableKeyPayload(value) {
|
|
|
1884
2024
|
|
|
1885
2025
|
// src/middleware/express.ts
|
|
1886
2026
|
var KNOWN_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2027
|
+
// Legacy UPPER_SNAKE codes (server-originated and SDK ≤2.6.x throws).
|
|
1887
2028
|
"TOKEN_INVALID",
|
|
1888
2029
|
"TOKEN_EXPIRED",
|
|
1889
2030
|
"TOKEN_REVOKED",
|
|
1890
2031
|
"SESSION_EXPIRED",
|
|
1891
2032
|
"SESSION_INVALID",
|
|
1892
|
-
"AUTH_REQUIRED"
|
|
2033
|
+
"AUTH_REQUIRED",
|
|
2034
|
+
// Task #127 — typed `IQAuthErrorCode` taxonomy thrown by `tokens.verify`.
|
|
2035
|
+
// Mapped to 401 here so framework consumers don't have to learn the new
|
|
2036
|
+
// codes to keep their auth-failure handling working.
|
|
2037
|
+
"token_invalid",
|
|
2038
|
+
"token_expired"
|
|
1893
2039
|
]);
|
|
1894
2040
|
var DEFAULT_ACCESS_COOKIE = "iqauth_at";
|
|
1895
2041
|
var DEFAULT_REFRESH_COOKIE = "iqauth_rt";
|
|
@@ -2073,6 +2219,41 @@ function iqAuthMiddleware(clientOrOptions, options = {}) {
|
|
|
2073
2219
|
}
|
|
2074
2220
|
|
|
2075
2221
|
// src/server/handlers.ts
|
|
2222
|
+
async function buildUserinfoResponse(claims, opts = {}) {
|
|
2223
|
+
const baseUser = {
|
|
2224
|
+
sub: claims.sub,
|
|
2225
|
+
email: claims.email,
|
|
2226
|
+
name: claims.name,
|
|
2227
|
+
tenantId: claims.tenantId,
|
|
2228
|
+
vendorId: claims.vendorId,
|
|
2229
|
+
roles: claims.roles ?? [],
|
|
2230
|
+
entitlements: claims.entitlements ?? []
|
|
2231
|
+
};
|
|
2232
|
+
const enriched = opts.enrich ? await opts.enrich(claims) : null;
|
|
2233
|
+
const user = enriched ? { ...baseUser, ...enriched } : baseUser;
|
|
2234
|
+
return {
|
|
2235
|
+
success: true,
|
|
2236
|
+
data: {
|
|
2237
|
+
user,
|
|
2238
|
+
claims,
|
|
2239
|
+
tenantId: claims.tenantId ?? null
|
|
2240
|
+
}
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
function emitTiming(cfg, event) {
|
|
2244
|
+
if (cfg.debug) {
|
|
2245
|
+
try {
|
|
2246
|
+
console.debug("[iqauth_helper]", event);
|
|
2247
|
+
} catch {
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
if (cfg.onTimingEvent) {
|
|
2251
|
+
try {
|
|
2252
|
+
cfg.onTimingEvent(event);
|
|
2253
|
+
} catch {
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2076
2257
|
var TERMINAL_REFRESH_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2077
2258
|
"TOKEN_REVOKED",
|
|
2078
2259
|
"SESSION_REVOKED",
|
|
@@ -2112,7 +2293,11 @@ function resolve(config) {
|
|
|
2112
2293
|
})),
|
|
2113
2294
|
appId: parsed.appId,
|
|
2114
2295
|
tenantId: parsed.tenantId,
|
|
2115
|
-
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only"
|
|
2296
|
+
clearCookiesOnRefreshFailure: config.clearCookiesOnRefreshFailure ?? "terminal-only",
|
|
2297
|
+
debug: config.debug,
|
|
2298
|
+
onTimingEvent: config.onTimingEvent,
|
|
2299
|
+
signoutRegistry: config.signoutRegistry ?? defaultSignoutRegistry,
|
|
2300
|
+
signoutMarkerTtlMs: config.signoutMarkerTtlMs ?? DEFAULT_SIGNOUT_TTL_MS
|
|
2116
2301
|
};
|
|
2117
2302
|
}
|
|
2118
2303
|
function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
@@ -2129,13 +2314,40 @@ function makeCookie(cfg, name, value, maxAge, httpOnly = true) {
|
|
|
2129
2314
|
}
|
|
2130
2315
|
function clearCookies(cfg) {
|
|
2131
2316
|
return [
|
|
2132
|
-
makeCookie(cfg, cfg.accessCookieName, "", 0),
|
|
2133
|
-
makeCookie(cfg, cfg.refreshCookieName, "", 0)
|
|
2317
|
+
{ ...makeCookie(cfg, cfg.accessCookieName, "", 0), clear: true },
|
|
2318
|
+
{ ...makeCookie(cfg, cfg.refreshCookieName, "", 0), clear: true }
|
|
2134
2319
|
];
|
|
2135
2320
|
}
|
|
2321
|
+
var DEFAULT_SIGNOUT_TTL_MS = 6e4;
|
|
2322
|
+
var inMemorySignoutMarkers = /* @__PURE__ */ new Map();
|
|
2323
|
+
function pruneInMemoryMarkers(now) {
|
|
2324
|
+
if (inMemorySignoutMarkers.size === 0) return;
|
|
2325
|
+
for (const [k, exp] of inMemorySignoutMarkers) {
|
|
2326
|
+
if (exp <= now) inMemorySignoutMarkers.delete(k);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
var defaultSignoutRegistry = {
|
|
2330
|
+
mark(token, ttlMs) {
|
|
2331
|
+
const now = Date.now();
|
|
2332
|
+
pruneInMemoryMarkers(now);
|
|
2333
|
+
inMemorySignoutMarkers.set(token, now + ttlMs);
|
|
2334
|
+
},
|
|
2335
|
+
has(token) {
|
|
2336
|
+
const now = Date.now();
|
|
2337
|
+
const exp = inMemorySignoutMarkers.get(token);
|
|
2338
|
+
if (!exp) return false;
|
|
2339
|
+
if (exp <= now) {
|
|
2340
|
+
inMemorySignoutMarkers.delete(token);
|
|
2341
|
+
return false;
|
|
2342
|
+
}
|
|
2343
|
+
return true;
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2136
2346
|
async function handleCallback(config, input) {
|
|
2137
2347
|
const cfg = resolve(config);
|
|
2348
|
+
const t0 = Date.now();
|
|
2138
2349
|
if (!input.code || !input.redirectUri) {
|
|
2350
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "VALIDATION_ERROR" });
|
|
2139
2351
|
return {
|
|
2140
2352
|
status: 400,
|
|
2141
2353
|
body: { success: false, error: { code: "VALIDATION_ERROR", message: "code and redirectUri are required" } },
|
|
@@ -2143,6 +2355,7 @@ async function handleCallback(config, input) {
|
|
|
2143
2355
|
};
|
|
2144
2356
|
}
|
|
2145
2357
|
if (!cfg.secretKey) {
|
|
2358
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: "INTERNAL_ERROR" });
|
|
2146
2359
|
return {
|
|
2147
2360
|
status: 500,
|
|
2148
2361
|
body: { success: false, error: { code: "INTERNAL_ERROR", message: "secretKey is required for the callback handler" } },
|
|
@@ -2166,6 +2379,7 @@ async function handleCallback(config, input) {
|
|
|
2166
2379
|
});
|
|
2167
2380
|
const json = await res.json().catch(() => ({}));
|
|
2168
2381
|
if (!res.ok || !json.access_token) {
|
|
2382
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: false, code: json.error || "OIDC_EXCHANGE_FAILED" });
|
|
2169
2383
|
return {
|
|
2170
2384
|
status: res.status || 502,
|
|
2171
2385
|
body: {
|
|
@@ -2185,6 +2399,7 @@ async function handleCallback(config, input) {
|
|
|
2185
2399
|
if (json.refresh_token) {
|
|
2186
2400
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.refresh_token, REFRESH_TOKEN_TTL_SECONDS));
|
|
2187
2401
|
}
|
|
2402
|
+
emitTiming(cfg, { phase: "callback", durationMs: Date.now() - t0, ok: true });
|
|
2188
2403
|
return {
|
|
2189
2404
|
status: 200,
|
|
2190
2405
|
body: { success: true, data: { authenticated: true } },
|
|
@@ -2193,8 +2408,18 @@ async function handleCallback(config, input) {
|
|
|
2193
2408
|
}
|
|
2194
2409
|
async function handleRefresh(config, input) {
|
|
2195
2410
|
const cfg = resolve(config);
|
|
2411
|
+
const t0 = Date.now();
|
|
2196
2412
|
const refreshToken = input.refreshToken;
|
|
2413
|
+
const idemKey = input.idempotencyToken;
|
|
2414
|
+
if (idemKey && await Promise.resolve(cfg.signoutRegistry.has(idemKey))) {
|
|
2415
|
+
return {
|
|
2416
|
+
status: 401,
|
|
2417
|
+
body: { success: false, error: { code: "SESSION_REVOKED", message: "Session was signed out" } },
|
|
2418
|
+
cookies: clearCookies(cfg)
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2197
2421
|
if (!refreshToken) {
|
|
2422
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: "TOKEN_INVALID" });
|
|
2198
2423
|
return {
|
|
2199
2424
|
status: 401,
|
|
2200
2425
|
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing refresh token" } },
|
|
@@ -2210,6 +2435,7 @@ async function handleRefresh(config, input) {
|
|
|
2210
2435
|
if (!res.ok || !json.success || !json.data?.accessToken) {
|
|
2211
2436
|
const status = res.status || 401;
|
|
2212
2437
|
const errorCode = json.error?.code || "TOKEN_INVALID";
|
|
2438
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: false, code: errorCode });
|
|
2213
2439
|
const shouldClear = shouldClearCookiesOnFailure(
|
|
2214
2440
|
cfg.clearCookiesOnRefreshFailure,
|
|
2215
2441
|
status,
|
|
@@ -2233,6 +2459,7 @@ async function handleRefresh(config, input) {
|
|
|
2233
2459
|
if (json.data.refreshToken) {
|
|
2234
2460
|
cookies.push(makeCookie(cfg, cfg.refreshCookieName, json.data.refreshToken, REFRESH_TOKEN_TTL_SECONDS));
|
|
2235
2461
|
}
|
|
2462
|
+
emitTiming(cfg, { phase: "refresh", durationMs: Date.now() - t0, ok: true });
|
|
2236
2463
|
return {
|
|
2237
2464
|
status: 200,
|
|
2238
2465
|
body: { success: true, data: { accessToken: json.data.accessToken } },
|
|
@@ -2241,6 +2468,10 @@ async function handleRefresh(config, input) {
|
|
|
2241
2468
|
}
|
|
2242
2469
|
async function handleSignout(config, input) {
|
|
2243
2470
|
const cfg = resolve(config);
|
|
2471
|
+
const t0 = Date.now();
|
|
2472
|
+
if (input.idempotencyToken) {
|
|
2473
|
+
await Promise.resolve(cfg.signoutRegistry.mark(input.idempotencyToken, cfg.signoutMarkerTtlMs));
|
|
2474
|
+
}
|
|
2244
2475
|
if (input.accessToken) {
|
|
2245
2476
|
try {
|
|
2246
2477
|
await cfg.fetchImpl(`${cfg.issuer}${cfg.logoutPath}`, {
|
|
@@ -2262,15 +2493,56 @@ async function handleSignout(config, input) {
|
|
|
2262
2493
|
} catch {
|
|
2263
2494
|
}
|
|
2264
2495
|
}
|
|
2496
|
+
emitTiming(cfg, { phase: "signout", durationMs: Date.now() - t0, ok: true });
|
|
2265
2497
|
return {
|
|
2266
2498
|
status: 200,
|
|
2267
2499
|
body: { success: true, data: { signedOut: true } },
|
|
2268
2500
|
cookies: clearCookies(cfg)
|
|
2269
2501
|
};
|
|
2270
2502
|
}
|
|
2503
|
+
var TOKENS_CACHE = /* @__PURE__ */ new Map();
|
|
2504
|
+
function getTokensFor(issuer) {
|
|
2505
|
+
let m = TOKENS_CACHE.get(issuer);
|
|
2506
|
+
if (!m) {
|
|
2507
|
+
m = new TokensModule(issuer);
|
|
2508
|
+
TOKENS_CACHE.set(issuer, m);
|
|
2509
|
+
}
|
|
2510
|
+
return m;
|
|
2511
|
+
}
|
|
2512
|
+
async function handleUserinfo(config, input) {
|
|
2513
|
+
const cfg = resolve(config);
|
|
2514
|
+
if (!input.accessToken) {
|
|
2515
|
+
return {
|
|
2516
|
+
status: 401,
|
|
2517
|
+
body: { success: false, error: { code: "TOKEN_INVALID", message: "Missing access token" } },
|
|
2518
|
+
cookies: []
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
let claims;
|
|
2522
|
+
try {
|
|
2523
|
+
claims = await getTokensFor(cfg.issuer).verify(input.accessToken, config.verify);
|
|
2524
|
+
} catch (err) {
|
|
2525
|
+
const code = err instanceof IQAuthError ? err.code : err.code || "TOKEN_INVALID";
|
|
2526
|
+
const message = err instanceof Error ? err.message : "Access token verification failed";
|
|
2527
|
+
return {
|
|
2528
|
+
status: 401,
|
|
2529
|
+
body: { success: false, error: { code, message } },
|
|
2530
|
+
cookies: []
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
const envelope = await buildUserinfoResponse(claims, {
|
|
2534
|
+
enrich: config.userinfoEnricher ? (c) => config.userinfoEnricher(c, input.req) : void 0
|
|
2535
|
+
});
|
|
2536
|
+
return {
|
|
2537
|
+
status: 200,
|
|
2538
|
+
body: envelope,
|
|
2539
|
+
cookies: []
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2271
2542
|
|
|
2272
2543
|
// src/express.ts
|
|
2273
2544
|
var PKCE_COOKIE = "iqauth_pkce";
|
|
2545
|
+
var IDEMPOTENCY_HEADER = "x-iqauth-idempotency";
|
|
2274
2546
|
function escapeHtml(s) {
|
|
2275
2547
|
return s.replace(/[&<>"']/g, (c) => {
|
|
2276
2548
|
switch (c) {
|
|
@@ -2344,6 +2616,17 @@ function defaultBrandedSpinner(args) {
|
|
|
2344
2616
|
}
|
|
2345
2617
|
function applyHandlerResponse(res, hr) {
|
|
2346
2618
|
for (const c of hr.cookies) {
|
|
2619
|
+
if (c.clear && typeof res.clearCookie === "function") {
|
|
2620
|
+
const opts = {
|
|
2621
|
+
httpOnly: c.httpOnly,
|
|
2622
|
+
secure: c.secure,
|
|
2623
|
+
sameSite: c.sameSite,
|
|
2624
|
+
path: c.path
|
|
2625
|
+
};
|
|
2626
|
+
if (c.domain) opts.domain = c.domain;
|
|
2627
|
+
res.clearCookie(c.name, opts);
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2347
2630
|
if (typeof res.cookie === "function") {
|
|
2348
2631
|
const opts = {
|
|
2349
2632
|
httpOnly: c.httpOnly,
|
|
@@ -2353,11 +2636,13 @@ function applyHandlerResponse(res, hr) {
|
|
|
2353
2636
|
maxAge: c.maxAge * 1e3
|
|
2354
2637
|
};
|
|
2355
2638
|
if (c.domain) opts.domain = c.domain;
|
|
2639
|
+
if (c.clear) opts.expires = /* @__PURE__ */ new Date(0);
|
|
2356
2640
|
res.cookie(c.name, c.value, opts);
|
|
2357
2641
|
} else {
|
|
2358
2642
|
const existing = res.getHeader?.("Set-Cookie") || [];
|
|
2359
2643
|
const list = Array.isArray(existing) ? existing : [existing];
|
|
2360
2644
|
const parts = [`${c.name}=${encodeURIComponent(c.value)}`, `Path=${c.path}`, `Max-Age=${c.maxAge}`, `SameSite=${c.sameSite}`];
|
|
2645
|
+
if (c.clear) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
2361
2646
|
if (c.secure) parts.push("Secure");
|
|
2362
2647
|
if (c.httpOnly) parts.push("HttpOnly");
|
|
2363
2648
|
if (c.domain) parts.push(`Domain=${c.domain}`);
|
|
@@ -2408,6 +2693,7 @@ function iqAuth(options) {
|
|
|
2408
2693
|
const inline = options.inlineCallback === true ? {} : options.inlineCallback && typeof options.inlineCallback === "object" ? options.inlineCallback : null;
|
|
2409
2694
|
const inlineBranded = inline?.branded === true ? {} : inline?.branded && typeof inline.branded === "object" ? inline.branded : null;
|
|
2410
2695
|
const attachHelpers = (app) => {
|
|
2696
|
+
void client.prewarm();
|
|
2411
2697
|
app.post(`${mount}/callback`, async (req, res) => {
|
|
2412
2698
|
const body = readBody(req);
|
|
2413
2699
|
const hr = await handleCallback(helperConfig, {
|
|
@@ -2545,13 +2831,23 @@ function iqAuth(options) {
|
|
|
2545
2831
|
app.post(`${mount}/refresh`, async (req, res) => {
|
|
2546
2832
|
const body = readBody(req);
|
|
2547
2833
|
const refreshToken = body.refreshToken || readCookieFromReq(req, refreshCookie);
|
|
2548
|
-
const
|
|
2834
|
+
const idempotencyToken = req.headers?.[IDEMPOTENCY_HEADER] || body.idempotencyToken;
|
|
2835
|
+
const hr = await handleRefresh(helperConfig, { refreshToken, idempotencyToken });
|
|
2549
2836
|
applyHandlerResponse(res, hr);
|
|
2550
2837
|
});
|
|
2838
|
+
if (options.mountUserinfo && typeof app.get === "function") {
|
|
2839
|
+
app.get(`${mount}/me`, async (req, res) => {
|
|
2840
|
+
const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
|
|
2841
|
+
const hr = await handleUserinfo(helperConfig, { accessToken, req });
|
|
2842
|
+
applyHandlerResponse(res, hr);
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2551
2845
|
app.post(`${mount}/signout`, async (req, res) => {
|
|
2552
2846
|
const accessToken = req.headers?.authorization?.replace(/^Bearer /i, "") || readCookieFromReq(req, accessCookie);
|
|
2847
|
+
const refreshToken = readCookieFromReq(req, refreshCookie);
|
|
2553
2848
|
const ssoCookieHeader = req.headers?.cookie;
|
|
2554
|
-
const
|
|
2849
|
+
const idempotencyToken = req.headers?.[IDEMPOTENCY_HEADER];
|
|
2850
|
+
const hr = await handleSignout(helperConfig, { accessToken, refreshToken, idempotencyToken, ssoCookieHeader });
|
|
2555
2851
|
applyHandlerResponse(res, hr);
|
|
2556
2852
|
});
|
|
2557
2853
|
};
|
|
@@ -2559,6 +2855,7 @@ function iqAuth(options) {
|
|
|
2559
2855
|
composed.middleware = middleware;
|
|
2560
2856
|
composed.attachHelpers = attachHelpers;
|
|
2561
2857
|
composed.client = client;
|
|
2858
|
+
composed.prewarm = () => client.prewarm();
|
|
2562
2859
|
return composed;
|
|
2563
2860
|
}
|
|
2564
2861
|
// Annotate the CommonJS export names for ESM import in node:
|